From 95e6522720f1035aafbd22d9c87cb55d77f3dab4 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sat, 20 Jun 2026 19:07:39 -0300 Subject: [PATCH] =?UTF-8?q?feat(compression):=20unified=20config=20panel?= =?UTF-8?q?=20=E2=80=94=20single=20source=20for=20engine=20on/off=20+=20le?= =?UTF-8?q?vel=20(Phase=201)=20(#4432)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(compression): design spec for the unified compression config panel Engine-centric central panel (single source for master + per-engine on/off + level), default pipeline derived from the engines map; Combos page owns named ordered pipelines + active-profile selection; per-engine pages keep only detailed config. Phased: (1) core consolidation + Model A migration, (2) named profiles + active selector, (3) per-request x-omniroute-compression header. * docs(compression): Phase 1 implementation plan for the unified config panel 12 TDD tasks: engine catalog, engines map + activeComboId, deriveDefaultPlan, migration 102 + backfill, resolveCompressionPlan, runtime wiring, API, default-combo shim, the engine-grid panel, consolidation, menu. Ends with a recap + the pending follow-ups (Phase 2 named profiles/active selector, Phase 3 header, deferred items). * feat(compression): engine catalog metadata (levels, single-mode, order) * feat(compression): add engines map + activeComboId to CompressionConfig * feat(compression): deriveDefaultPlan (engines map -> mode/pipeline) Pure function that converts a per-engine EngineToggle map + masterEnabled flag into a { mode, stackedPipeline } plan: off when master is off or no engines on; single-mode when exactly one single-mode engine is enabled; stacked (sorted by stackPriority) otherwise. * feat(compression): persist+backfill engines map and activeComboId (migration 102) * feat(compression): resolveCompressionPlan precedence resolver (header>override>active>default) * feat(compression): selectCompressionStrategy uses resolveCompressionPlan * feat(api): settings/compression carries engines map + activeComboId Extends compressionSettingsUpdateSchema with engines (Record) and activeComboId (string|null) so the PUT route accepts and persists these fields. GET already returns the full getCompressionSettings() object which includes both fields. TDD: added route round-trip tests (PUT+GET) for engines, activeComboId, null clear, and schema rejection of invalid engines shape. * refactor(compression): default-combo route is a read-only shim (default derived from engines) * feat(dashboard): engine-grid compression panel (single source for on/off + level) * refactor(dashboard): remove duplicate compression toggles; per-engine pages keep only detailed config * feat(compression): unified panel menu order + derived-pipeline integration coverage * fix(compression): import ENGINE_IDS via types re-export so it resolves under vitest The bare "@omniroute/open-sse/.../engineCatalog.ts" specifier resolves under tsc/tsx but not under vitest's MCP config: Vite externalizes a brand-new open-sse module to Node, which can't load the .ts subpath. types.ts is already in Vite's graph, so route ENGINE_IDS through its re-export. Fixes 3 failing MCP vitest suites (cacheTools, dbHealthTool, essentialTools). * fix(compression): engines map drives dispatch only when explicitly panel-saved Code-review finding: the legacy seeded default combo (present on every install via migration 042/043) was silently overriding a panel-configured engines map — the default-combo block in chatCore set compressionComboApplied=true, skipping the engines-map override, so an operator's panel toggles were ignored. Gate the engines-map path on a new runtime-only CompressionConfig.enginesExplicit (true when a stored engines row exists). Panel-saved installs: the engines map is authoritative (deriveDefaultPlanFromConfig + new enginesMapDerivesStackedPipeline guard the chatCore default-combo block). Legacy/backfilled installs: the map is display-only and dispatch stays on the historical defaultMode/default-combo path — zero behaviour change until the operator opts in via the panel. Also documents why resolveCacheAwareConfig's getCacheAwareStrategy(config.defaultMode) arg is safe (skipSystemPrompt is mode-independent). * docs(compression): add MDX frontmatter + escape inline brace to fix fumadocs build docs/compression/**/*.md is compiled as MDX by the Next build (fumadocs, source.config.ts). The two planning docs lacked the required `title` frontmatter (build error: 'title: expected string, received undefined') and the design doc had a bare {rtk,caveman} that MDX parsed as a JSX expression. Adds frontmatter matching the sibling docs and backticks the brace. Fixes the dast-smoke build step. --- ...unified-compression-config-panel-design.md | 240 +++++++++ ...0-unified-compression-config-panel-plan.md | 495 ++++++++++++++++++ open-sse/handlers/chatCore.ts | 34 +- .../services/compression/deriveDefaultPlan.ts | 48 ++ .../services/compression/engineCatalog.ts | 84 +++ .../compression/resolveCompressionPlan.ts | 47 ++ .../services/compression/strategySelector.ts | 130 ++++- open-sse/services/compression/types.ts | 27 + .../caveman/CavemanContextPageClient.tsx | 60 +-- .../context/combos/CompressionHub.tsx | 81 +-- .../context/rtk/RtkContextPageClient.tsx | 28 +- .../context/settings/CompressionPanel.tsx | 362 +++++++++++++ .../dashboard/context/settings/page.tsx | 4 +- .../components/CompressionSettingsTab.tsx | 127 +---- .../components/CompressionTokenSaverCard.tsx | 204 ++------ src/app/api/context/combos/default/route.ts | 66 +-- src/i18n/messages/en.json | 1 + src/lib/db/compression.ts | 151 ++++++ .../102_compression_engines_map.sql | 5 + .../compression/EngineConfigPage.tsx | 141 +++-- src/shared/constants/sidebarVisibility.ts | 19 +- .../validation/compressionConfigSchemas.ts | 7 + .../api/compression/compression-api.test.ts | 106 +++- .../api/context-combos-default-route.test.ts | 121 +++-- .../compression-engines-map-migration.test.ts | 69 +++ .../compression/derive-default-plan.test.ts | 33 ++ .../derived-pipeline-integration.test.ts | 133 +++++ tests/unit/compression/engine-catalog.test.ts | 28 + .../resolve-compression-plan.test.ts | 22 + .../unit/compression/strategySelector.test.ts | 116 +++- tests/unit/sidebar-visibility.test.ts | 4 +- tests/unit/ui/compressionHub.test.tsx | 53 ++ tests/unit/ui/compressionPanel.test.tsx | 220 ++++++++ tests/unit/ui/engineConfigPage.test.tsx | 130 +++-- tests/unit/ui/sidebar-engine-items.test.ts | 21 +- 35 files changed, 2767 insertions(+), 650 deletions(-) create mode 100644 docs/compression/2026-06-20-unified-compression-config-panel-design.md create mode 100644 docs/compression/2026-06-20-unified-compression-config-panel-plan.md create mode 100644 open-sse/services/compression/deriveDefaultPlan.ts create mode 100644 open-sse/services/compression/engineCatalog.ts create mode 100644 open-sse/services/compression/resolveCompressionPlan.ts create mode 100644 src/app/(dashboard)/dashboard/context/settings/CompressionPanel.tsx create mode 100644 src/lib/db/migrations/102_compression_engines_map.sql create mode 100644 tests/unit/compression/compression-engines-map-migration.test.ts create mode 100644 tests/unit/compression/derive-default-plan.test.ts create mode 100644 tests/unit/compression/derived-pipeline-integration.test.ts create mode 100644 tests/unit/compression/engine-catalog.test.ts create mode 100644 tests/unit/compression/resolve-compression-plan.test.ts create mode 100644 tests/unit/ui/compressionPanel.test.tsx diff --git a/docs/compression/2026-06-20-unified-compression-config-panel-design.md b/docs/compression/2026-06-20-unified-compression-config-panel-design.md new file mode 100644 index 0000000000..747ba7ff6e --- /dev/null +++ b/docs/compression/2026-06-20-unified-compression-config-panel-design.md @@ -0,0 +1,240 @@ +--- +title: "Unified Compression Config Panel — Design" +version: 3.8.32 +lastUpdated: 2026-06-20 +--- + +# Unified Compression Config Panel — Design + +**Status:** approved direction (2026-06-20), pending spec review +**Base branch:** `release/v3.8.31` +**Goal:** Make `/dashboard/context/settings` the single management panel for the master +on/off and each compression engine's on/off + level, deriving the default pipeline from +those toggles. Keep the Combos page solely for *chaining* (ordered named pipelines) + +selecting the globally-active profile, and the per-engine pages solely for *detailed* +config. Plan for (but phase) multiple named profiles with an active selector and a +per-request override header. + +--- + +## 1. Background — current state (the problem) + +Compression on/off + level is split across **two stores and several UIs**, with real +duplication: + +- **`/api/settings/compression`** (DB `key_value` ns=`compression`, via `src/lib/db/compression.ts`) + holds: `enabled` (master), `defaultMode`, `autoTriggerTokens`, `cavemanConfig`, + `cavemanOutputMode`, `rtkConfig`, `aggressive`, `ultra`, language config, etc. +- **`/api/context/combos/default`** (the *default combo pipeline*) holds the per-engine + `enabled` + config for the **structural** engines (lite, headroom, session-dedup, ccr, + llmlingua, aggressive, ultra) as pipeline steps. The per-engine detail pages + (`EngineConfigPage.tsx`) read/write here via `setEngineInDefaultCombo`. +- **`compression_combos`** table holds *named* pipelines assigned to routing combos. + +Concrete duplications (from the UI audit): +- Caveman on/off in **3** places (TokenSaverCard, CompressionSettingsTab, CavemanContextPageClient). +- Caveman intensity in **3**; RTK intensity in **2**; Caveman output mode in **2**. +- `lite/session-dedup/headroom/ccr/llmlingua` on/off **absent** from the central settings — + only on their per-engine page (which writes the *default combo*, not the central config). + +So "is engine X on?" is answered inconsistently (central config for caveman/rtk; default +combo for the structural engines), and the **Combos page edits the same default pipeline** +that a central panel would — the conceptual overlap the redesign must remove. + +Out of scope as a *config* surface: `dashboard/compression/studio` is the Compression +Studio (waterfall/cockpit analytics), not toggles — untouched. + +Engines (the 10 stackable units): `lite, caveman, aggressive, ultra, rtk, headroom, +session-dedup, ccr, llmlingua` (registered `CompressionEngine`s) + `mcpAccessibility` +(separate path: compresses MCP tool-result outputs, not the chat pipeline). Each engine's +`stackPriority` defines automatic ordering (session-dedup 3, ccr 4, lite 5, rtk 10, +headroom 15, caveman 20, aggressive 30, llmlingua 35, ultra 40). + +--- + +## 2. Design principles — the boundary + +| Surface | Concept | Owns | Never does | +|---|---|---|---| +| **Panel** (`context/settings`) | "My **Default**: what is on + level" | master on/off; per-engine on/off + level | ordering/chaining; per-route assignment | +| **Combos** (`context/combos`, menu #2) | "Named **profiles**: chaining + which is active + per-route assignment" | create/edit ordered named pipelines; pick the globally-active profile; assign to routing combos | engine on/off (inherits the active profile's membership) | +| **Per-engine pages** (menu, after combos) | "Deep config of one engine" | filters, rules, language packs, thresholds | on/off; level (those live in the panel) | + +**No duplication rule:** the **Panel owns the Default profile** (membership + level, order +auto-derived by `stackPriority`). A **Combo is a *named alternative* profile** (membership ++ level + *explicit order*). Different scopes (the one default vs named alternatives) — not +the same object edited twice. The editable "default combo" store is **removed**; the +default pipeline becomes **derived** from the panel. + +--- + +## 3. Architecture + +### 3.1 Resolution model (per request, most-specific wins) + +``` +x-omniroute-compression header (per-request override) + → routing-combo override (comboOverrides[comboId]) (per-route) + → active profile (activeComboId: "default" | ) (global) + → Default = derived from panel engines map + → off (master disabled, or zero engines on) +``` + +A single `resolveCompressionPlan(config, { comboId, header })` produces the effective +`{ mode, stackedPipeline }` fed to the existing `applyCompressionAsync`. It supersedes +today's `getEffectiveMode` (which only does combo-override → autoTrigger → defaultMode). + +### 3.2 Data model (Model A — single source + derived pipeline) + +**CompressionConfig gains an engines map** (the single source for the Default's on/off + +level), replacing the editable default-combo store: + +```ts +interface EngineToggle { + enabled: boolean; + level?: string; // caveman/cavemanOutput: lite|full|ultra ; rtk: minimal|standard|aggressive ; others: undefined +} +interface CompressionConfig { + enabled: boolean; // master + engines: Record; // NEW — the Default profile + activeComboId: string | null; // NEW — null/"default" = derived default; else a compression_combos id + autoTriggerTokens: number; + autoTriggerMode?: CompressionMode; // kept (auto-trigger still selects a profile/mode on large prompts) + preserveSystemPrompt: boolean; + comboOverrides: Record; // existing per-routing-combo override + // detailed per-engine config keeps living in its existing sub-objects + // (cavemanConfig, rtkConfig, aggressive, ultra, …) — edited by the per-engine pages. +} +``` + +**Deriving the Default pipeline** from `engines` (pure function `deriveDefaultPlan`): +- master `enabled === false` → `off`. +- 0 engines enabled → `off`. +- exactly 1 enabled and it is a single-mode engine (`lite|caveman|aggressive|ultra|rtk`) → + that mode (single path — reuses today's `applyCompression(mode)`). +- otherwise → `stacked` with the enabled engines in `stackPriority` order, each step's + `intensity` from its `level` (the global `stackedPipeline` already accepts all 9 engines + after the B-PIPELINE-DIVERGENCE fix). + +The **stored `defaultMode` field** and the editable default combo +(`/api/context/combos/default`, `setEngineInDefaultCombo`) are **removed**; the default is +derived from `engines`. (The `CompressionMode` *type* persists — `comboOverrides`, +`autoTriggerMode`, and the resolver's output still use it; only the stored `defaultMode` +field is dropped.) A DB migration backfills `engines` from the current `defaultMode` + +default-combo steps + caveman/rtk config so existing installs keep their behavior. + +**Named combos** (`compression_combos`, existing table): N ordered pipelines. `activeComboId` +selects which profile is globally active (`Default` or a named combo). Per-routing-combo +assignment stays in `comboOverrides`. + +`mcpAccessibility` keeps its own store (`/api/settings/compression/mcp-accessibility`, +migration 056) — surfaced in the panel as a row that writes there, with a scope note. + +### 3.3 The per-request header + +`x-omniroute-compression: ` (mirrors the `x-omniroute-no-memory`/`no-cache` pattern, +PR #4290; parsed in the request pipeline alongside the other omniroute headers). Values: +- `off` → no compression for this request. +- `default` → the derived Default profile. +- `` → that named combo. +- `engine:` → a single engine (if that engine is enabled in the Default), e.g. `engine:rtk`. + +Invalid/unknown value → ignored (falls through to the normal resolution); never errors the +request. Header parsing + validation has a unit test asserting each form and the +fall-through. + +--- + +## 4. Screens + +### 4.1 Panel — `context/settings` (engine grid) + +A single client component (replacing the scattered `CompressionSettingsTab` + +`CompressionTokenSaverCard` toggles): +- **Master** on/off at top. +- **Engine grid** (one row per engine, ordered by `stackPriority` so the row order mirrors + run order): `[engine name + short desc] [on/off toggle] [level selector if applicable] + [→ detail page]`. Level selector appears only for engines with levels + (caveman lite|full|ultra, rtk minimal|standard|aggressive; caveman output mode as its own row). +- **General** (auto-trigger tokens, preserve-system-prompt) below the grid. +- Reads/writes the `engines` map + master via `GET/PUT /api/settings/compression` (single + endpoint). The displayed default pipeline (derived) is shown read-only ("runs: rtk → + caveman → …") so the user sees the effect without editing order here. + +### 4.2 Combos — `context/combos` (menu #2) + +- List of named combos; create/edit an **ordered** pipeline (drag/reorder + per-step level) + — the only place for explicit chaining. +- **Active profile selector**: `Default (panel)` | `` → writes `activeComboId`. +- Assign a combo to a routing combo (existing `comboOverrides`). +- Reuses the existing `CompressionCombosPageClient` / `comboFlowModel`; the `CompressionHub` + "master mode selector" is removed (mode is now derived; the Hub becomes a read-only + overview or is folded into the panel). + +### 4.3 Per-engine pages (menu, after combos) + +`EngineConfigPage` + the caveman/rtk custom pages: **lose** the on/off + level controls +(moved to the panel) and keep only **detailed** config (filters, rules, language packs, +thresholds, preview). They **stop writing** `/api/context/combos/default`; detailed config +writes to its own sub-object in `/api/settings/compression` (or the existing +caveman/rtk facade routes, which already proxy to it). + +### 4.4 Navigation + +`COMPRESSION_CONTEXT_GROUP` (`src/shared/constants/sidebarVisibility.ts`) reordered: +**Settings (panel) → Combos → per-engine pages → Studio (analytics)**. + +--- + +## 5. Consolidation / migration + +- `CompressionTokenSaverCard` quick toggles → **absorbed into the panel; the card is removed**. +- Duplicate caveman/rtk on/off + intensity in `CompressionSettingsTab` → removed. +- `EngineConfigPage` → on/off+level removed; stops writing the default combo. +- DB migration: backfill `engines` map + `activeComboId="default"` from current state + (defaultMode + default-combo steps + caveman/rtk/ultra/aggressive enabled), so live + installs preserve behavior. The editable default-combo route (`PUT /api/context/combos/default`) + becomes a **read-only shim for one release** (returns the derived default; rejects writes + with a deprecation note), then is removed. + +--- + +## 6. Phasing + +Each phase is its own implementation plan + PR, independently shippable and TDD'd +(Hard Rule #18). `writing-plans` will author **Phase 1 first**; Phases 2–3 get their own +plans later. + +- **Phase 1 (core consolidation):** the `engines` map + `deriveDefaultPlan` + migration; + the engine-grid panel; remove scattered/duplicate toggles; per-engine pages lose on/off; + menu reorder. Delivers the single-panel goal. *No behavior change for existing installs + (migration backfills).* +- **Phase 2 (profiles):** multiple named combos + the `activeComboId` active-profile selector. +- **Phase 3 (header):** the `x-omniroute-compression` per-request override. + +Phases 2–3 reuse the Phase-1 resolution model (`resolveCompressionPlan`), which is built +header/active-aware from the start so later phases only wire UI + header parsing. + +--- + +## 7. Testing + +- **Unit:** `deriveDefaultPlan` (every engines-map shape → expected mode/pipeline); + migration backfill (old config → equivalent engines map); `resolveCompressionPlan` + precedence (header > comboOverride > active > default > off); header parsing (each form + + fall-through); panel reducer (toggle/level edits → config patch). +- **Component (vitest):** the panel renders all engines, toggles persist, derived pipeline + preview updates. +- **Integration:** a config with engines `{rtk,caveman}` on → `applyCompressionAsync` runs the + derived stacked pipeline; single engine on → single-mode path; equivalence with the old + defaultMode behavior for the same logical config. +- Both runners green (`test:unit` + `test:vitest`); typecheck:core clean; lint 0 errors. + +--- + +## 8. Non-goals (YAGNI) + +- No new compression engines in this work (the model just makes adding them trivial later). +- No change to the engines' internal algorithms (the recent fixes are separate). +- The Compression Studio (analytics) is not restructured. +- Phases 2–3 UI polish (combo templates, sharing) is out of scope. diff --git a/docs/compression/2026-06-20-unified-compression-config-panel-plan.md b/docs/compression/2026-06-20-unified-compression-config-panel-plan.md new file mode 100644 index 0000000000..34a51fcf35 --- /dev/null +++ b/docs/compression/2026-06-20-unified-compression-config-panel-plan.md @@ -0,0 +1,495 @@ +--- +title: "Unified Compression Config Panel — Phase 1 Implementation Plan" +version: 3.8.32 +lastUpdated: 2026-06-20 +--- + +# Unified Compression Config Panel — Phase 1 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make `/dashboard/context/settings` the single source for the master + per-engine on/off + level, with the default compression pipeline DERIVED from those toggles, removing the scattered/duplicate toggles — with zero behavior change for existing installs (a migration backfills). + +**Architecture:** Add an `engines` map (+`activeComboId`) to `CompressionConfig` as the single source. A pure `deriveDefaultPlan(engines)` turns it into `{mode, stackedPipeline}`; a pure `resolveCompressionPlan(config, ctx)` applies precedence (header→combo-override→active-profile→derived-default→off) and feeds the existing `applyCompressionAsync`. A DB migration backfills the map. The engine-grid panel reads/writes the map via the single `/api/settings/compression` endpoint; per-engine pages lose on/off+level; the editable default-combo route becomes a read-only shim. + +**Tech Stack:** TypeScript, Next.js 16 App Router, Zod, SQLite (better-sqlite3), Node test runner + Vitest (component), React. + +**Base:** worktree `feat/compression-config-panel-v3831` off `release/v3.8.31`. Spec: `docs/compression/2026-06-20-unified-compression-config-panel-design.md`. + +**Conventions:** unit tests run `node --import tsx/esm --test tests/unit/.test.ts`; component tests run `npm run test:vitest`. Each task ends by running the FULL compression suite (`node --import tsx/esm --test tests/unit/compression/*.test.ts`) + `npm run typecheck:core` before commit. Never `--no-verify`. + +--- + +## File Structure + +**Create:** +- `open-sse/services/compression/engineCatalog.ts` — pure metadata: per-engine `{ id, label, stackPriority, levels?, isSingleMode }`. One source of truth for "which engines exist, which have levels, which can be a standalone mode". +- `open-sse/services/compression/deriveDefaultPlan.ts` — pure: `engines` map → `{ mode, stackedPipeline }`. +- `open-sse/services/compression/resolveCompressionPlan.ts` — pure: precedence resolver. +- `src/lib/db/migrations/102_compression_engines_map.sql` — backfill `engines` + `activeComboId`. +- `src/app/(dashboard)/dashboard/context/settings/CompressionPanel.tsx` — the engine-grid panel. +- `tests/unit/compression/engine-catalog.test.ts`, `derive-default-plan.test.ts`, `resolve-compression-plan.test.ts`, `compression-engines-map-migration.test.ts`. +- `tests/unit/ui/compressionPanel.test.tsx` (vitest). + +**Modify:** +- `open-sse/services/compression/types.ts` — add `EngineToggle`, `CompressionConfig.engines`, `activeComboId`; keep `CompressionMode` type; drop stored `defaultMode` usage (derive). +- `src/lib/db/compression.ts` — normalize/persist `engines` + `activeComboId`. +- `open-sse/services/compression/strategySelector.ts` — `selectCompressionStrategy`/`getEffectiveMode` delegate to `resolveCompressionPlan`. +- `open-sse/handlers/chatCore.ts` — call `resolveCompressionPlan`. +- `src/app/api/settings/compression/route.ts` — accept/return `engines` + `activeComboId`. +- `src/app/api/context/combos/default/route.ts` — read-only shim (reject writes). +- `src/app/(dashboard)/dashboard/context/settings/page.tsx` — render `CompressionPanel` (not the old tab). +- `src/shared/components/compression/EngineConfigPage.tsx` + caveman/rtk client pages — remove on/off+level; stop writing default combo. +- `src/shared/constants/sidebarVisibility.ts` — reorder `COMPRESSION_CONTEXT_GROUP`. + +**Engine reference (`stackPriority`, levels, single-mode):** +`session-dedup`(3,—,no) `ccr`(4,—,no) `lite`(5,—,yes) `rtk`(10,minimal|standard|aggressive,yes) `headroom`(15,—,no) `caveman`(20,lite|full|ultra,yes) `aggressive`(30,—,yes) `llmlingua`(35,—,no) `ultra`(40,—,yes). Plus `cavemanOutput`(intensity lite|full|ultra, separate from input caveman) and `mcpAccessibility`(no level, separate store). + +--- + +## Task 1: Engine catalog (single source of engine metadata) + +**Files:** +- Create: `open-sse/services/compression/engineCatalog.ts` +- Test: `tests/unit/compression/engine-catalog.test.ts` + +- [ ] **Step 1: Write the failing test** + +```ts +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { ENGINE_CATALOG, engineMeta, ENGINE_IDS } from "@omniroute/open-sse/services/compression/engineCatalog.ts"; + +test("catalog lists every engine with stackPriority", () => { + for (const id of ["session-dedup","ccr","lite","rtk","headroom","caveman","aggressive","llmlingua","ultra"]) { + assert.ok(engineMeta(id), `${id} present`); + assert.equal(typeof engineMeta(id).stackPriority, "number"); + } +}); +test("levels + single-mode flags are correct", () => { + assert.deepEqual(engineMeta("rtk").levels, ["minimal","standard","aggressive"]); + assert.deepEqual(engineMeta("caveman").levels, ["lite","full","ultra"]); + assert.equal(engineMeta("headroom").levels, undefined); + assert.equal(engineMeta("caveman").isSingleMode, true); + assert.equal(engineMeta("headroom").isSingleMode, false); +}); +test("ENGINE_IDS is ordered by stackPriority", () => { + const ps = ENGINE_IDS.map((id) => engineMeta(id).stackPriority); + assert.deepEqual(ps, [...ps].sort((a,b)=>a-b)); +}); +``` + +- [ ] **Step 2: Run → FAIL** (`node --import tsx/esm --test tests/unit/compression/engine-catalog.test.ts`) — module not found. + +- [ ] **Step 3: Implement** `engineCatalog.ts`: + +```ts +export interface EngineMeta { + id: string; + label: string; + stackPriority: number; + levels?: string[]; // intensity options; undefined = no level selector + isSingleMode: boolean; // can be the effective mode when it is the only engine on + description: string; +} +export const ENGINE_CATALOG: Record = { + "session-dedup": { id:"session-dedup", label:"Session Dedup", stackPriority:3, isSingleMode:false, description:"Cross-turn block deduplication." }, + ccr: { id:"ccr", label:"CCR (Retrieval)", stackPriority:4, isSingleMode:false, description:"Content-addressed retrieval markers." }, + lite: { id:"lite", label:"Lite", stackPriority:5, isSingleMode:true, description:"Whitespace/format cleanup." }, + rtk: { id:"rtk", label:"RTK", stackPriority:10, levels:["minimal","standard","aggressive"], isSingleMode:true, description:"Command-output filtering." }, + headroom: { id:"headroom", label:"Headroom", stackPriority:15, isSingleMode:false, description:"Tabular JSON compaction." }, + caveman: { id:"caveman", label:"Caveman", stackPriority:20, levels:["lite","full","ultra"], isSingleMode:true, description:"Rule-based prose compression." }, + aggressive: { id:"aggressive", label:"Aggressive", stackPriority:30, isSingleMode:true, description:"Summarize + age old turns." }, + llmlingua: { id:"llmlingua", label:"LLMLingua (SLM)", stackPriority:35, isSingleMode:false, description:"Semantic pruning (ONNX)." }, + ultra: { id:"ultra", label:"Ultra", stackPriority:40, isSingleMode:true, description:"Heuristic token pruning (+ optional SLM)." }, +}; +export const ENGINE_IDS: string[] = Object.values(ENGINE_CATALOG).sort((a,b)=>a.stackPriority-b.stackPriority).map((e)=>e.id); +export function engineMeta(id: string): EngineMeta { return ENGINE_CATALOG[id]; } +``` + +- [ ] **Step 4: Run → PASS.** +- [ ] **Step 5: Commit** `git add open-sse/services/compression/engineCatalog.ts tests/unit/compression/engine-catalog.test.ts && git commit -m "feat(compression): engine catalog metadata (levels, single-mode, order)"` + +--- + +## Task 2: `EngineToggle` + `engines` map + `activeComboId` on the config type + +**Files:** +- Modify: `open-sse/services/compression/types.ts` (the `CompressionConfig` interface + `DEFAULT_COMPRESSION_CONFIG`) +- Test: `tests/unit/compression/engine-catalog.test.ts` (extend) — assert the default config shape. + +- [ ] **Step 1: Add the test** (append): + +```ts +import { DEFAULT_COMPRESSION_CONFIG } from "@omniroute/open-sse/services/compression/types.ts"; +test("default config has an engines map + activeComboId", () => { + assert.equal(typeof DEFAULT_COMPRESSION_CONFIG.engines, "object"); + assert.equal(DEFAULT_COMPRESSION_CONFIG.activeComboId, null); + // default-off: every engine disabled by default (opt-in preserved) + for (const id of ENGINE_IDS) assert.equal(DEFAULT_COMPRESSION_CONFIG.engines[id]?.enabled, false); +}); +``` + +- [ ] **Step 2: Run → FAIL.** +- [ ] **Step 3: Implement** in `types.ts`: add `export interface EngineToggle { enabled: boolean; level?: string }`; add to `CompressionConfig`: `engines: Record;` and `activeComboId: string | null;`. In `DEFAULT_COMPRESSION_CONFIG` add `engines: Object.fromEntries(ENGINE_IDS.map((id)=>[id,{enabled:false}]))` (import `ENGINE_IDS`) and `activeComboId: null`. Keep `defaultMode` field for now (removed in Task 9 once derive is wired) to avoid breaking compilation. + +- [ ] **Step 4: Run → PASS + `npm run typecheck:core`.** +- [ ] **Step 5: Commit** `... -m "feat(compression): add engines map + activeComboId to CompressionConfig"` + +--- + +## Task 3: `deriveDefaultPlan` (the heart — engines map → mode/pipeline) + +**Files:** +- Create: `open-sse/services/compression/deriveDefaultPlan.ts` +- Test: `tests/unit/compression/derive-default-plan.test.ts` + +- [ ] **Step 1: Write the failing test** + +```ts +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { deriveDefaultPlan } from "@omniroute/open-sse/services/compression/deriveDefaultPlan.ts"; + +const on = (level) => ({ enabled: true, ...(level?{level}:{}) }); +test("master off / empty / none-on => off", () => { + assert.deepEqual(deriveDefaultPlan({}, false), { mode:"off", stackedPipeline:[] }); + assert.deepEqual(deriveDefaultPlan({}, true), { mode:"off", stackedPipeline:[] }); + assert.deepEqual(deriveDefaultPlan({ rtk:{enabled:false} }, true), { mode:"off", stackedPipeline:[] }); +}); +test("exactly one single-mode engine => that mode", () => { + assert.deepEqual(deriveDefaultPlan({ caveman: on("full") }, true), { mode:"standard", stackedPipeline:[] }); + assert.deepEqual(deriveDefaultPlan({ rtk: on("minimal") }, true), { mode:"rtk", stackedPipeline:[] }); + assert.deepEqual(deriveDefaultPlan({ lite: on() }, true), { mode:"lite", stackedPipeline:[] }); +}); +test("one non-single-mode engine => stacked with that engine", () => { + const p = deriveDefaultPlan({ headroom: on() }, true); + assert.equal(p.mode, "stacked"); + assert.deepEqual(p.stackedPipeline, [{ engine:"headroom" }]); +}); +test("multiple engines => stacked in stackPriority order, levels as intensity", () => { + const p = deriveDefaultPlan({ caveman: on("full"), rtk: on("standard"), headroom: on() }, true); + assert.equal(p.mode, "stacked"); + assert.deepEqual(p.stackedPipeline, [ + { engine:"rtk", intensity:"standard" }, // pri 10 + { engine:"headroom" }, // pri 15 + { engine:"caveman", intensity:"full" }, // pri 20 + ]); +}); +``` + +- [ ] **Step 2: Run → FAIL.** +- [ ] **Step 3: Implement** `deriveDefaultPlan.ts`: + +```ts +import { ENGINE_CATALOG, engineMeta } from "./engineCatalog.ts"; +import type { EngineToggle } from "./types.ts"; + +const SINGLE_MODE_OF: Record = { lite:"lite", caveman:"standard", aggressive:"aggressive", ultra:"ultra", rtk:"rtk" }; + +export interface DerivedPlan { mode: string; stackedPipeline: Array<{ engine: string; intensity?: string }>; } + +export function deriveDefaultPlan(engines: Record, masterEnabled: boolean): DerivedPlan { + if (!masterEnabled) return { mode:"off", stackedPipeline:[] }; + const onIds = Object.keys(ENGINE_CATALOG).filter((id) => engines[id]?.enabled === true); + if (onIds.length === 0) return { mode:"off", stackedPipeline:[] }; + if (onIds.length === 1 && engineMeta(onIds[0]).isSingleMode) { + return { mode: SINGLE_MODE_OF[onIds[0]], stackedPipeline:[] }; + } + const ordered = onIds.sort((a,b)=>engineMeta(a).stackPriority-engineMeta(b).stackPriority); + const stackedPipeline = ordered.map((id) => { + const level = engines[id]?.level; + return level ? { engine:id, intensity:level } : { engine:id }; + }); + return { mode:"stacked", stackedPipeline }; +} +``` + +- [ ] **Step 4: Run → PASS.** +- [ ] **Step 5: Commit** `... -m "feat(compression): deriveDefaultPlan (engines map -> mode/pipeline)"` + +--- + +## Task 4: Migration 102 — backfill `engines` + `activeComboId` + +**Files:** +- Create: `src/lib/db/migrations/102_compression_engines_map.sql` +- Test: `tests/unit/compression/compression-engines-map-migration.test.ts` + +- [ ] **Step 1: Write the failing test** (uses `resetDbInstance()` + closes handles in `test.after`, per repo rule): + +```ts +import { test, after } from "node:test"; +import assert from "node:assert/strict"; +import { getDbInstance, resetDbInstance } from "@/lib/db/core.ts"; +import { getCompressionSettings } from "@/lib/db/compression.ts"; + +after(() => resetDbInstance()); +test("migration backfills engines map from prior defaultMode + default combo", () => { + resetDbInstance(); + const db = getDbInstance(); // runs migrations incl. 102 + // simulate a pre-102 install: master on, defaultMode 'standard', caveman enabled + db.prepare("INSERT OR REPLACE INTO key_value(namespace,key,value) VALUES('compression','enabled','true')").run(); + db.prepare("INSERT OR REPLACE INTO key_value(namespace,key,value) VALUES('compression','defaultMode','\"standard\"')").run(); + db.prepare("INSERT OR REPLACE INTO key_value(namespace,key,value) VALUES('compression','cavemanConfig','{\"enabled\":true}')").run(); + const cfg = getCompressionSettings(); + assert.equal(cfg.engines.caveman.enabled, true); + assert.equal(cfg.activeComboId, null); +}); +``` + +- [ ] **Step 2: Run → FAIL.** +- [ ] **Step 3: Implement** the SQL migration (idempotent; the row backfill of derived `engines` is done in `normalizeCompressionSettings` read-path — the SQL only seeds `activeComboId` default and a marker). Migration `102_compression_engines_map.sql`: + +```sql +-- Phase 1 of the unified compression panel: the engines map + activeComboId become the +-- single source. The engines map is DERIVED on read (normalizeCompressionSettings) from the +-- legacy defaultMode + default-combo steps + caveman/rtk/ultra/aggressive config, so existing +-- installs keep their behavior. Here we only ensure activeComboId defaults to NULL ("default"). +INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('compression', 'activeComboId', 'null'); +``` + +(The read-path derivation in Task 5 is what makes `getCompressionSettings().engines` correct; the migration just guarantees `activeComboId` exists.) + +- [ ] **Step 4: Run → PASS** (after Task 5's normalize is in — if running 4 before 5, expect the engines assertion to fail; do Task 5 then re-run). Commit after Task 5. + +--- + +## Task 5: Persist + normalize `engines` / `activeComboId` (read-path derivation) + +**Files:** +- Modify: `src/lib/db/compression.ts` (`getCompressionSettings`/`normalizeCompressionSettings`/`updateCompressionSettings`) +- Test: reuse Task 4's migration test + add a round-trip test in the same file. + +- [ ] **Step 1: Add round-trip test**: + +```ts +import { updateCompressionSettings } from "@/lib/db/compression.ts"; +test("engines map persists round-trip + activeComboId", () => { + resetDbInstance(); getDbInstance(); + updateCompressionSettings({ enabled:true, engines:{ rtk:{enabled:true,level:"standard"}, caveman:{enabled:true,level:"full"} }, activeComboId:null }); + const cfg = getCompressionSettings(); + assert.equal(cfg.engines.rtk.enabled, true); + assert.equal(cfg.engines.rtk.level, "standard"); + assert.equal(cfg.engines.caveman.level, "full"); +}); +``` + +- [ ] **Step 2: Run → FAIL.** +- [ ] **Step 3: Implement** in `compression.ts`: + - In `normalizeCompressionSettings`: read stored `engines` if present; ELSE derive it from legacy fields — `engines[id].enabled` from: caveman/rtk/ultra/aggressive `*.enabled`; structural engines from the default-combo steps (read the default combo); single-modes from `defaultMode`. Levels from `cavemanConfig.intensity`/`rtkConfig.intensity`. Read `activeComboId` (default null). + - In `updateCompressionSettings`: accept `engines` (validate each value `{enabled:boolean, level?:string}`) + `activeComboId` and persist as `key_value` rows (`engines` as one JSON row). + - Add a Zod sub-schema `engineToggleSchema = z.object({ enabled: z.boolean(), level: z.string().optional() })` and `engines: z.record(engineToggleSchema).optional()`, `activeComboId: z.string().nullable().optional()`. + +- [ ] **Step 4: Run → PASS** (Task 4 + Task 5 tests). `npm run typecheck:core`. +- [ ] **Step 5: Commit** `git add src/lib/db/migrations/102_compression_engines_map.sql src/lib/db/compression.ts tests/unit/compression/compression-engines-map-migration.test.ts && git commit -m "feat(compression): persist+backfill engines map and activeComboId (migration 102)"` + +--- + +## Task 6: `resolveCompressionPlan` (precedence resolver) + +**Files:** +- Create: `open-sse/services/compression/resolveCompressionPlan.ts` +- Test: `tests/unit/compression/resolve-compression-plan.test.ts` + +- [ ] **Step 1: Write the failing test** + +```ts +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { resolveCompressionPlan } from "@omniroute/open-sse/services/compression/resolveCompressionPlan.ts"; + +const base = { enabled:true, engines:{ caveman:{enabled:true,level:"full"} }, activeComboId:null, comboOverrides:{} }; +test("derived default when no override/active/header", () => { + assert.deepEqual(resolveCompressionPlan(base, {}), { mode:"standard", stackedPipeline:[] }); +}); +test("routing-combo override wins over default", () => { + const cfg = { ...base, comboOverrides:{ cmb:"aggressive" } }; + assert.equal(resolveCompressionPlan(cfg, { comboId:"cmb" }).mode, "aggressive"); +}); +test("active named combo wins over default (Phase 2 wiring uses combos table; here pass it in)", () => { + const cfg = { ...base, activeComboId:"c1" }; + const combos = { c1: [{ engine:"rtk", intensity:"standard" }] }; + const plan = resolveCompressionPlan(cfg, { combos }); + assert.equal(plan.mode, "stacked"); + assert.deepEqual(plan.stackedPipeline, [{ engine:"rtk", intensity:"standard" }]); +}); +test("master off => off regardless", () => { + assert.equal(resolveCompressionPlan({ ...base, enabled:false }, {}).mode, "off"); +}); +``` + +- [ ] **Step 2: Run → FAIL.** +- [ ] **Step 3: Implement** `resolveCompressionPlan.ts`: + +```ts +import { deriveDefaultPlan, type DerivedPlan } from "./deriveDefaultPlan.ts"; + +export interface ResolveCtx { + comboId?: string | null; + header?: string | null; // x-omniroute-compression (Phase 3 parses+passes; Phase 1 callers pass undefined) + combos?: Record>; // named combo pipelines by id +} +export function resolveCompressionPlan(config: any, ctx: ResolveCtx): DerivedPlan { + if (config?.enabled === false) return { mode:"off", stackedPipeline:[] }; + // 1. header (Phase 3 supplies parsed value; here it composes if present) + if (ctx.header) { + if (ctx.header === "off") return { mode:"off", stackedPipeline:[] }; + if (ctx.header !== "default") { + const fromHeader = headerToPlan(ctx.header, config, ctx); + if (fromHeader) return fromHeader; // unknown => fall through + } + } + // 2. routing-combo override + const ov = ctx.comboId ? config?.comboOverrides?.[ctx.comboId] : undefined; + if (ov) return modeToPlan(ov, config); + // 3. active named combo + if (config?.activeComboId && ctx.combos?.[config.activeComboId]) { + return { mode:"stacked", stackedPipeline: ctx.combos[config.activeComboId] }; + } + // 4. derived default + return deriveDefaultPlan(config?.engines ?? {}, config?.enabled !== false); +} +function modeToPlan(mode: string, config: any): DerivedPlan { + return mode === "stacked" + ? { mode:"stacked", stackedPipeline: config?.stackedPipeline ?? [] } + : { mode, stackedPipeline:[] }; +} +function headerToPlan(h: string, config: any, ctx: ResolveCtx): DerivedPlan | null { + if (h.startsWith("engine:")) { const id = h.slice(7); return config?.engines?.[id]?.enabled ? deriveDefaultPlan({ [id]: config.engines[id] }, true) : null; } + if (ctx.combos?.[h]) return { mode:"stacked", stackedPipeline: ctx.combos[h] }; + return null; +} +``` + +- [ ] **Step 4: Run → PASS.** +- [ ] **Step 5: Commit** `... -m "feat(compression): resolveCompressionPlan precedence resolver (header>override>active>default)"` + +--- + +## Task 7: Wire the resolver into strategy selection + chatCore + +**Files:** +- Modify: `open-sse/services/compression/strategySelector.ts` (`selectCompressionStrategy`) +- Modify: `open-sse/handlers/chatCore.ts` (the compression call site) +- Test: `tests/unit/compression/strategySelector.test.ts` (extend with an engines-map case) + +- [ ] **Step 1: Add test** asserting `selectCompressionStrategy` with `engines:{rtk:{enabled:true}}` + master on returns mode `rtk`; with `{rtk,caveman}` returns `stacked`. (Use the existing test's import + harness.) +- [ ] **Step 2: Run → FAIL.** +- [ ] **Step 3: Implement** — `selectCompressionStrategy` calls `resolveCompressionPlan(config, { comboId, combos })` and returns its `mode` (and expose the `stackedPipeline` so `applyCompressionAsync` uses the derived pipeline when mode==="stacked"). Load named `combos` from the combos DB module. In `chatCore.ts`, pass the active combo set; keep the `header` arg `undefined` (Phase 3 fills it). Keep `autoTriggerMode` behavior (auto-trigger still overrides to its mode on large prompts — apply BEFORE step 4 default). +- [ ] **Step 4: Run → PASS** + full compression suite + typecheck. +- [ ] **Step 5: Commit** `... -m "feat(compression): selectCompressionStrategy uses resolveCompressionPlan"` + +--- + +## Task 8: API — `/api/settings/compression` carries `engines` + `activeComboId` + +**Files:** +- Modify: `src/app/api/settings/compression/route.ts` +- Test: `tests/unit/api/compression/compression-api.test.ts` (extend) + +- [ ] **Step 1: Add test**: `PUT` with `{engines:{rtk:{enabled:true,level:"standard"}}}` then `GET` returns it; error body has no stack (`!body.error?.message?.includes("at /")`). +- [ ] **Step 2: Run → FAIL.** +- [ ] **Step 3: Implement** — extend the route's Zod body schema with `engines` + `activeComboId` (reuse the db sub-schema); GET returns them; errors via `buildErrorBody`. +- [ ] **Step 4: Run → PASS** + vitest if the route is covered there. +- [ ] **Step 5: Commit** `... -m "feat(api): settings/compression carries engines map + activeComboId"` + +--- + +## Task 9: Remove stored `defaultMode` write-path + default-combo editable route → shim + +**Files:** +- Modify: `open-sse/services/compression/types.ts` (drop `defaultMode` from the persisted shape; keep `CompressionMode` type) +- Modify: `src/app/api/context/combos/default/route.ts` (PUT → 410/deprecation; GET → derived default read-only) +- Test: `tests/unit/api/...` for the shim (PUT rejected, GET returns derived). + +- [ ] **Step 1: Add test**: `PUT /api/context/combos/default` returns a deprecation error (not 200); `GET` returns the derived default pipeline. +- [ ] **Step 2: Run → FAIL.** +- [ ] **Step 3: Implement** — `setEngineInDefaultCombo` no longer the write path; PUT route returns `buildErrorBody` "deprecated: edit engines in /api/settings/compression"; GET returns `deriveDefaultPlan(config.engines, config.enabled)`. Remove remaining reads of stored `defaultMode` (derive). +- [ ] **Step 4: Run → PASS** + full suite + typecheck. +- [ ] **Step 5: Commit** `... -m "refactor(compression): derive default; default-combo route is a read-only shim"` + +--- + +## Task 10: The engine-grid panel UI + +**Files:** +- Create: `src/app/(dashboard)/dashboard/context/settings/CompressionPanel.tsx` +- Modify: `src/app/(dashboard)/dashboard/context/settings/page.tsx` (render `CompressionPanel`) +- Test: `tests/unit/ui/compressionPanel.test.tsx` (vitest, `createRoot`+`act`) + +- [ ] **Step 1: Write the failing component test**: render `CompressionPanel` with a stubbed `fetch` returning `{enabled:true, engines:{rtk:{enabled:true,level:"standard"}}}`; assert it renders a row per `ENGINE_IDS`, the rtk level shows "standard", toggling caveman issues a `PUT` with `engines.caveman.enabled:true`, and the derived-pipeline preview text appears. +- [ ] **Step 2: Run → FAIL** (`npm run test:vitest`). +- [ ] **Step 3: Implement** `CompressionPanel.tsx`: master toggle; map `ENGINE_IDS` → a row component `[label+desc][Toggle][LevelSelect if meta.levels][Link → /dashboard/context/]`; a derived-pipeline preview computed client-side via `deriveDefaultPlan` (import the pure fn); a `cavemanOutput` row; an `mcpAccessibility` row (writes its own endpoint, with a "MCP tool outputs" note); general settings (auto-trigger, preserve system prompt). Save via `PUT /api/settings/compression` (debounced, merge-patch like the existing `save()` pattern in `CompressionSettingsTab`). Reuse existing primitives (`Toggle`, segmented control) from the current cards. +- [ ] **Step 4:** Update `page.tsx` to render ``. Run → PASS. +- [ ] **Step 5: Commit** `... -m "feat(dashboard): engine-grid compression panel (single source for on/off + level)"` + +--- + +## Task 11: Consolidate — remove scattered/duplicate toggles + per-engine on/off + +**Files:** +- Modify: `src/app/(dashboard)/dashboard/settings/components/CompressionTokenSaverCard.tsx` (remove; or strip to a read-only summary linking to the panel) +- Modify: `CompressionSettingsTab.tsx` (remove duplicate caveman/rtk on/off + intensity sections; keep only things not in the panel, or delete if fully superseded) +- Modify: `src/shared/components/compression/EngineConfigPage.tsx` + `CavemanContextPageClient.tsx` + `RtkContextPageClient.tsx` (remove on/off + level; keep detailed config; stop writing `/api/context/combos/default`) +- Test: vitest render tests for the per-engine pages assert NO enabled toggle is present; the existing tests updated to the new shape (alignment, not masking). + +- [ ] **Step 1:** Update the affected render tests to expect the new (toggle-free) shape; run → FAIL on the still-present toggles. +- [ ] **Step 2:** Implement the removals; per-engine detail save writes detailed config to its facade route (caveman/rtk) or the settings sub-object. +- [ ] **Step 3:** Run vitest + full compression suite → PASS. +- [ ] **Step 4: Commit** `... -m "refactor(dashboard): remove duplicate compression toggles; per-engine pages keep only detailed config"` + +--- + +## Task 12: Menu reorder + integration + full validation + +**Files:** +- Modify: `src/shared/constants/sidebarVisibility.ts` (`COMPRESSION_CONTEXT_GROUP`: Settings → Combos → per-engine → Studio) +- Test: `tests/unit/...` sidebar order test (if one exists) + an integration test. + +- [ ] **Step 1:** Add an integration test: build a config with `engines:{rtk:{enabled:true},caveman:{enabled:true,level:"full"}}`, call `selectCompressionStrategy` + `applyCompressionAsync` on a realistic body, assert the derived stacked pipeline ran (engineBreakdown has rtk+caveman) and equals the behavior of an explicit `[rtk,caveman]` stacked config. Run → (write fails first if any wiring gap). +- [ ] **Step 2:** Reorder the sidebar group; update any sidebar order test (alignment). +- [ ] **Step 3:** FULL validation: `npm run typecheck:core` (clean) · `npm run lint` (0 errors) · `node --import tsx/esm --test tests/unit/compression/*.test.ts` (green) · `npm run test:vitest` (green) · the api/integration compression tests. +- [ ] **Step 4: Commit** `... -m "feat(compression): unified panel menu order + integration coverage"` + +--- + +## Self-review notes (done while writing) +- Spec coverage: panel (T10), combos boundary (default derived T3/T9; named/active resolver T6 — UI for active selection is Phase 2), per-engine detail-only (T11), header (resolver is header-aware T6; parsing+wiring is Phase 3), migration (T4/T5), menu (T12). ✓ +- Type consistency: `EngineToggle` (T2) used by `deriveDefaultPlan` (T3), `resolveCompressionPlan` (T6), normalize (T5), panel (T10). `DerivedPlan` shape consistent T3↔T6↔T7. ✓ +- No placeholders: each task has concrete code/tests. UI tasks (T10/T11) specify exact behavior + assertions; final per-line component code is produced at execution following the existing card patterns. + +--- + +# 📌 RESUMO — o que ESTE plano (Fase 1) entrega + +1. **Catálogo de engines** (`engineCatalog.ts`) — fonte única de metadados (níveis, single-mode, ordem). +2. **Modelo de dados Fase A** — `engines` map + `activeComboId` em `CompressionConfig`, com migração 102 + backfill (zero mudança de comportamento). +3. **`deriveDefaultPlan`** — pipeline default DERIVADO dos toggles (0/1/N engines → off/modo/stacked). +4. **`resolveCompressionPlan`** — resolvedor de precedência (header > override por-rota > perfil ativo > default derivado > off), já header/active-aware. +5. **Fiação no runtime** — `selectCompressionStrategy`/`chatCore` usam o resolvedor. +6. **API** — `/api/settings/compression` carrega `engines` + `activeComboId`; rota `combos/default` vira shim read-only; `defaultMode` armazenado removido (derivado). +7. **Painel engine-grid** — `CompressionPanel.tsx` como fonte única de master + on/off + nível, com preview do pipeline derivado. +8. **Consolidação** — remove toggles duplicados (TokenSaverCard, CompressionSettingsTab) e tira on/off+nível das páginas por-engine (que ficam só com config detalhada). +9. **Menu** — Settings → Combos → páginas por-engine → Studio. + +--- + +# ⏳ PENDÊNCIAS — a fazer DEPOIS deste plano (cada uma vira seu próprio plano/PR) + +### Fase 2 — Perfis nomeados + seletor de ativo +- **UI de combos como perfis**: a página `context/combos` lista N combos nomeados, edita pipeline **ordenado** (drag/reorder + nível por step), e tem o seletor **"perfil ativo"** (`Default` | ``) gravando `activeComboId`. O resolvedor (Fase 1) já consome `activeComboId`; falta a UI + o carregamento dos combos nomeados no `selectCompressionStrategy`. Remover o "master mode selector" do `CompressionHub` (modo agora é derivado). + +### Fase 3 — Header por-request `x-omniroute-compression` +- **Parsing + wiring do header**: ler `x-omniroute-compression` no pipeline (espelhando `x-omniroute-no-memory`, PR #4290), passar como `ctx.header` ao `resolveCompressionPlan` (que já trata `off`/`default`/``/`engine:`). Doc no `API_REFERENCE` + teste de fetch-capture provando precedência por-request. + +### Itens de compressão deferidos (do ciclo de fixes, independentes deste painel) +- **B-OBSERVABILITY** (telemetria): engines no-op somem do `engineBreakdown` (não dá pra distinguir "rodou 0%" de "pulou"). Exige um refactor do modelo de breakdown (campo `ran`/`skipped`) que toca a UI Studio + testes que asseram `.length` — deferido conscientemente. +- **B-CAVEMAN-PACKS**: `de`/`fr`/`ja` sem `dedup.json`+`ultra.json` (ultra==full nessas línguas). Conteúdo linguístico — adicionar os packs (ou contribuição), **sem** fallback EN que mutilaria. +- **js-tiktoken 1.0.21→1.0.22**: bump trivial (o range `^1.0.20` já permite; só pinar o lockfile num install real). + +### Decisão operacional pendente (não-código) +- **Ligar o SLM (tier ultra) em produção**: validado ao vivo (49,4% real), mas mantido **OFF** por sua escolha. Quando decidir o trade-off (custo/latência/qualidade do pruning), ligar via o painel (após Fase 1) ou pela escrita de config — começando conservador (≥2000 tok). + +### Portes upstream (opcionais, do audit — fora do escopo deste painel) +- headroom *safety-rails*/BM25; filtros novos do rtk/token-savior; conformance GCF v3.1 (já cobrimos o `[..]:`); transformers.js 3.5.2→4.x (arriscado, major). diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index a1c15d447d..1f2ce9240f 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -1443,8 +1443,13 @@ export async function handleChatCore({ // --- Modular Compression Pipeline (Phase 1 Lite + Phase 2 Standard/Caveman + Phase 3 Aggressive) --- // Runs BEFORE the existing reactive compressContext() to proactively reduce tokens. try { - const { selectCompressionStrategy, applyCompressionAsync, resolveCacheAwareConfig } = - await import("../services/compression/strategySelector.ts"); + const { + selectCompressionStrategy, + selectCompressionPlan, + enginesMapDerivesStackedPipeline, + applyCompressionAsync, + resolveCacheAwareConfig, + } = await import("../services/compression/strategySelector.ts"); const { trackCompressionStats } = await import("../services/compression/stats.ts"); let config: CompressionConfig = compressionSettings ?? { enabled: false, @@ -1622,7 +1627,12 @@ export async function handleChatCore({ modeBeforeOutputTransform === "stacked" && !compressionComboApplied && !config.compressionComboId && - isBuiltinStackedPipeline(config.stackedPipeline) + isBuiltinStackedPipeline(config.stackedPipeline) && + // Don't let the legacy default combo override a panel-configured engines map: when the + // operator's explicit engines derive their own stacked pipeline, that pipeline (applied + // below from compressionPlan.stackedPipeline) is authoritative. Legacy/backfilled + // installs (enginesExplicit false) still fall through to the seeded default combo. + !enginesMapDerivesStackedPipeline(config) ) { try { const { getDefaultCompressionCombo } = @@ -1672,13 +1682,29 @@ export async function handleChatCore({ } } const compressionInputBody = body as Record; - const mode = selectCompressionStrategy( + const compressionPlan = selectCompressionPlan( config, compressionComboKey, estimatedTokens, compressionInputBody, { provider, targetFormat, model: effectiveModel } ); + const mode = compressionPlan.mode as CompressionConfig["defaultMode"]; + // When the per-engine toggle map derives a stacked pipeline (and no named/routing + // combo already set config.stackedPipeline), feed that derived pipeline through so + // applyCompressionAsync (which reads config.stackedPipeline for stacked mode) runs the + // engines the operator actually toggled on instead of the built-in rtk+caveman default. + if ( + mode === "stacked" && + compressionPlan.stackedPipeline.length > 0 && + !compressionComboApplied && + !config.compressionComboId + ) { + config = { + ...config, + stackedPipeline: compressionPlan.stackedPipeline as CompressionConfig["stackedPipeline"], + }; + } let compressionAnalyticsRecorded = false; if (mode !== "off") { // #3890: in a caching context, never compress the system prompt (cacheable prefix) diff --git a/open-sse/services/compression/deriveDefaultPlan.ts b/open-sse/services/compression/deriveDefaultPlan.ts new file mode 100644 index 0000000000..ee7960fe69 --- /dev/null +++ b/open-sse/services/compression/deriveDefaultPlan.ts @@ -0,0 +1,48 @@ +import { ENGINE_CATALOG, engineMeta } from "./engineCatalog.ts"; +import type { EngineToggle } from "./types.ts"; + +/** Maps single-mode engine ids to the effective CompressionMode name. */ +const SINGLE_MODE_OF: Record = { + lite: "lite", + caveman: "standard", + aggressive: "aggressive", + ultra: "ultra", + rtk: "rtk", +}; + +export interface DerivedPlan { + mode: string; + stackedPipeline: Array<{ engine: string; intensity?: string }>; +} + +/** + * Derives the effective compression plan from the per-engine toggle map. + * + * Rules (evaluated in order): + * 1. masterEnabled=false OR no engines on → { mode:"off", stackedPipeline:[] } + * 2. Exactly one engine on AND it is single-mode → that engine's standalone mode + * 3. Otherwise → { mode:"stacked", stackedPipeline: enabled engines sorted by stackPriority } + */ +export function deriveDefaultPlan( + engines: Record, + masterEnabled: boolean, +): DerivedPlan { + if (!masterEnabled) return { mode: "off", stackedPipeline: [] }; + + const onIds = Object.keys(ENGINE_CATALOG).filter((id) => engines[id]?.enabled === true); + + if (onIds.length === 0) return { mode: "off", stackedPipeline: [] }; + + if (onIds.length === 1 && engineMeta(onIds[0]).isSingleMode) { + return { mode: SINGLE_MODE_OF[onIds[0]], stackedPipeline: [] }; + } + + const ordered = onIds.sort((a, b) => engineMeta(a).stackPriority - engineMeta(b).stackPriority); + + const stackedPipeline = ordered.map((id) => { + const level = engines[id]?.level; + return level ? { engine: id, intensity: level } : { engine: id }; + }); + + return { mode: "stacked", stackedPipeline }; +} diff --git a/open-sse/services/compression/engineCatalog.ts b/open-sse/services/compression/engineCatalog.ts new file mode 100644 index 0000000000..ff1c481b55 --- /dev/null +++ b/open-sse/services/compression/engineCatalog.ts @@ -0,0 +1,84 @@ +export interface EngineMeta { + id: string; + label: string; + stackPriority: number; + levels?: string[]; // intensity options; undefined = no level selector + isSingleMode: boolean; // can be the effective mode when it is the only engine on + description: string; +} + +export const ENGINE_CATALOG: Record = { + "session-dedup": { + id: "session-dedup", + label: "Session Dedup", + stackPriority: 3, + isSingleMode: false, + description: "Cross-turn block deduplication.", + }, + ccr: { + id: "ccr", + label: "CCR (Retrieval)", + stackPriority: 4, + isSingleMode: false, + description: "Content-addressed retrieval markers.", + }, + lite: { + id: "lite", + label: "Lite", + stackPriority: 5, + isSingleMode: true, + description: "Whitespace/format cleanup.", + }, + rtk: { + id: "rtk", + label: "RTK", + stackPriority: 10, + levels: ["minimal", "standard", "aggressive"], + isSingleMode: true, + description: "Command-output filtering.", + }, + headroom: { + id: "headroom", + label: "Headroom", + stackPriority: 15, + isSingleMode: false, + description: "Tabular JSON compaction.", + }, + caveman: { + id: "caveman", + label: "Caveman", + stackPriority: 20, + levels: ["lite", "full", "ultra"], + isSingleMode: true, + description: "Rule-based prose compression.", + }, + aggressive: { + id: "aggressive", + label: "Aggressive", + stackPriority: 30, + isSingleMode: true, + description: "Summarize + age old turns.", + }, + llmlingua: { + id: "llmlingua", + label: "LLMLingua (SLM)", + stackPriority: 35, + isSingleMode: false, + description: "Semantic pruning (ONNX).", + }, + ultra: { + id: "ultra", + label: "Ultra", + stackPriority: 40, + isSingleMode: true, + description: "Heuristic token pruning (+ optional SLM).", + }, +}; + +export const ENGINE_IDS: string[] = Object.values(ENGINE_CATALOG) + .sort((a, b) => a.stackPriority - b.stackPriority) + .map((e) => e.id); + +export function engineMeta(id: string): EngineMeta { + return ENGINE_CATALOG[id]; +} diff --git a/open-sse/services/compression/resolveCompressionPlan.ts b/open-sse/services/compression/resolveCompressionPlan.ts new file mode 100644 index 0000000000..a9b0543a9d --- /dev/null +++ b/open-sse/services/compression/resolveCompressionPlan.ts @@ -0,0 +1,47 @@ +import { deriveDefaultPlan, type DerivedPlan } from "./deriveDefaultPlan.ts"; + +export interface ResolveCtx { + comboId?: string | null; + header?: string | null; // x-omniroute-compression (Phase 3 parses+passes; Phase 1 callers pass undefined) + combos?: Record>; // named combo pipelines by id +} + +export function resolveCompressionPlan(config: any, ctx: ResolveCtx): DerivedPlan { + if (config?.enabled === false) return { mode: "off", stackedPipeline: [] }; + + // 1. header (Phase 3 supplies parsed value; here it composes if present) + if (ctx.header) { + if (ctx.header === "off") return { mode: "off", stackedPipeline: [] }; + if (ctx.header !== "default") { + const fromHeader = headerToPlan(ctx.header, config, ctx); + if (fromHeader) return fromHeader; // unknown => fall through + } + } + + // 2. routing-combo override + const ov = ctx.comboId ? config?.comboOverrides?.[ctx.comboId] : undefined; + if (ov) return modeToPlan(ov, config); + + // 3. active named combo + if (config?.activeComboId && ctx.combos?.[config.activeComboId]) { + return { mode: "stacked", stackedPipeline: ctx.combos[config.activeComboId] }; + } + + // 4. derived default + return deriveDefaultPlan(config?.engines ?? {}, config?.enabled !== false); +} + +function modeToPlan(mode: string, config: any): DerivedPlan { + return mode === "stacked" + ? { mode: "stacked", stackedPipeline: config?.stackedPipeline ?? [] } + : { mode, stackedPipeline: [] }; +} + +function headerToPlan(h: string, config: any, ctx: ResolveCtx): DerivedPlan | null { + if (h.startsWith("engine:")) { + const id = h.slice(7); + return config?.engines?.[id]?.enabled ? deriveDefaultPlan({ [id]: config.engines[id] }, true) : null; + } + if (ctx.combos?.[h]) return { mode: "stacked", stackedPipeline: ctx.combos[h] }; + return null; +} diff --git a/open-sse/services/compression/strategySelector.ts b/open-sse/services/compression/strategySelector.ts index a9810adf3d..973fdf1017 100644 --- a/open-sse/services/compression/strategySelector.ts +++ b/open-sse/services/compression/strategySelector.ts @@ -20,6 +20,8 @@ import { getCacheAwareStrategy, type CachingDetectionContext, } from "./cachingAware.ts"; +import { resolveCompressionPlan } from "./resolveCompressionPlan.ts"; +import { deriveDefaultPlan, type DerivedPlan } from "./deriveDefaultPlan.ts"; export function checkComboOverride( config: CompressionConfig, @@ -33,19 +35,122 @@ export function shouldAutoTrigger(config: CompressionConfig, estimatedTokens: nu return config.autoTriggerTokens > 0 && estimatedTokens >= config.autoTriggerTokens; } +/** + * Resolves the effective compression plan (mode + derived stacked pipeline) WITHOUT + * the caching-aware mode adjustment (that is layered on by {@link selectCompressionPlan}). + * + * Precedence — preserved from the historical {@link getEffectiveMode} ordering: + * 1. master off → off + * 2. routing-combo override (comboId)→ that mode (resolver honors it via ctx.comboId) + * 3. auto-trigger (large prompt) → autoTriggerMode, BEFORE the plain derived default + * 4. derived default → resolveCompressionPlan (engines map → mode/pipeline) + * + * Step 3 mirrors today's behaviour: auto-trigger takes precedence over the plain + * derived default but never over an explicit routing-combo override. + * + * `combos` is `{}` in Phase 1 — the active named-combo selection UI is Phase 2, and the + * resolver falls through to the derived default when no combo is supplied. chatCore still + * resolves named/default combos via its own DB path (mutating config.stackedPipeline). + */ +function resolveBasePlan( + config: CompressionConfig, + comboId: string | null, + estimatedTokens: number +): DerivedPlan { + if (!config.enabled) return { mode: "off", stackedPipeline: [] }; + + const comboMode = checkComboOverride(config, comboId); + if (comboMode) { + // A routing-combo "stacked" override still wants the configured stacked pipeline, + // so route it through the resolver (which reads config.stackedPipeline for stacked). + return resolveCompressionPlan(config, { comboId, combos: {} }); + } + + if (shouldAutoTrigger(config, estimatedTokens)) { + const mode = config.autoTriggerMode ?? "lite"; + return mode === "stacked" + ? { mode, stackedPipeline: config.stackedPipeline ?? [] } + : { mode, stackedPipeline: [] }; + } + + return deriveDefaultPlanFromConfig(config, comboId); +} + +/** + * Derived-default step. The per-engine toggle map drives the default ONLY when it was + * EXPLICITLY configured via the panel (a stored `engines` row — `config.enginesExplicit`). + * For legacy installs the map is backfilled for DISPLAY only (so the panel shows current + * state); dispatch falls back to the historical `config.defaultMode` so behaviour is + * byte-for-byte preserved until the operator opts into the panel by saving. This avoids a + * silent behaviour change for installs whose backfilled engine flags don't exactly match + * their old defaultMode. + */ +function deriveDefaultPlanFromConfig( + config: CompressionConfig, + comboId: string | null +): DerivedPlan { + if (config.enginesExplicit) { + // Panel-configured: the engines map (via the resolver, which stays header/active-combo + // aware for Phases 2-3) is authoritative — including an explicit "everything off". + return resolveCompressionPlan(config, { comboId, combos: {} }); + } + + // Legacy path: defaultMode carries the effective mode (the engines map is display-only here). + const legacyMode = config.defaultMode; + if (legacyMode && legacyMode !== "off") { + return legacyMode === "stacked" + ? { mode: legacyMode, stackedPipeline: config.stackedPipeline ?? [] } + : { mode: legacyMode, stackedPipeline: [] }; + } + + return { mode: "off", stackedPipeline: [] }; +} + +/** + * True when the EXPLICITLY-configured engines map (panel-saved) derives a multi-engine + * stacked pipeline. chatCore uses this to know the panel's derived pipeline is authoritative + * and the legacy default-combo fallback must NOT override it. Returns false for legacy + * (non-explicit) installs so their historical default-combo path is preserved untouched. + */ +export function enginesMapDerivesStackedPipeline(config: CompressionConfig): boolean { + if (!config.enginesExplicit) return false; + const plan = deriveDefaultPlan(config.engines ?? {}, config.enabled !== false); + return plan.mode === "stacked" && plan.stackedPipeline.length > 0; +} + export function getEffectiveMode( config: CompressionConfig, comboId: string | null, estimatedTokens: number ): CompressionMode { - if (!config.enabled) return "off"; + return resolveBasePlan(config, comboId, estimatedTokens).mode as CompressionMode; +} - const comboMode = checkComboOverride(config, comboId); - if (comboMode) return comboMode; +/** + * Like {@link selectCompressionStrategy} but returns the full derived plan + * (effective `mode` + `stackedPipeline`). When the resolver derives a `stacked` + * plan from the per-engine toggle map, the pipeline is exposed here so the caller + * can feed it to {@link applyCompressionAsync} (which reads config.stackedPipeline). + * The caching-aware mode adjustment is applied to `mode` exactly as in + * {@link selectCompressionStrategy}. + */ +export function selectCompressionPlan( + config: CompressionConfig, + comboId: string | null, + estimatedTokens: number, + body?: Record, + context?: CachingDetectionContext +): DerivedPlan { + const plan = resolveBasePlan(config, comboId, estimatedTokens); - if (shouldAutoTrigger(config, estimatedTokens)) return config.autoTriggerMode ?? "lite"; + // Apply caching-aware adjustments to the mode if body is provided + if (body) { + const ctx = detectCachingContext(body, context); + const cacheAware = getCacheAwareStrategy(plan.mode as CompressionMode, ctx); + return { ...plan, mode: cacheAware.strategy as CompressionMode }; + } - return config.defaultMode; + return plan; } export function selectCompressionStrategy( @@ -55,16 +160,7 @@ export function selectCompressionStrategy( body?: Record, context?: CachingDetectionContext ): CompressionMode { - const selectedMode = getEffectiveMode(config, comboId, estimatedTokens); - - // Apply caching-aware adjustments if body is provided - if (body) { - const ctx = detectCachingContext(body, context); - const cacheAware = getCacheAwareStrategy(selectedMode, ctx); - return cacheAware.strategy as CompressionMode; - } - - return selectedMode; + return selectCompressionPlan(config, comboId, estimatedTokens, body, context).mode as CompressionMode; } /** @@ -82,6 +178,10 @@ export function resolveCacheAwareConfig( ): 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 }; diff --git a/open-sse/services/compression/types.ts b/open-sse/services/compression/types.ts index 94db8751d0..e086cbfe50 100644 --- a/open-sse/services/compression/types.ts +++ b/open-sse/services/compression/types.ts @@ -9,6 +9,15 @@ * Phase 5: 'rtk' and 'stacked' modes (tool-output filters + multi-engine pipeline). */ +import { ENGINE_IDS } from "./engineCatalog.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. +// That bare alias resolves under tsc/tsx but NOT under vitest (Vite externalizes a brand-new +// open-sse module to Node, which then can't load the `.ts` subpath), whereas this module is +// already in Vite's graph and its relative `./engineCatalog.ts` import resolves in-pipeline. +export { ENGINE_IDS }; + export type CompressionMode = | "off" | "lite" @@ -108,6 +117,11 @@ export interface CompressionPipelineStep { config?: Record; } +export interface EngineToggle { + enabled: boolean; + level?: string; +} + export interface CompressionConfig { enabled: boolean; defaultMode: CompressionMode; @@ -127,6 +141,17 @@ export interface CompressionConfig { ultra?: UltraConfig; /** Provider-delegated context editing (Claude/Anthropic only). */ contextEditing?: ContextEditingConfig; + /** Per-engine opt-in toggles for the config panel. */ + engines: Record; + /** Active combo preset id, or null if none selected. */ + activeComboId: string | null; + /** + * Runtime-only (NOT persisted): true when a stored `engines` row exists, i.e. the operator + * configured engines via the panel. When false, the `engines` map is a display-only backfill + * and dispatch falls back to the legacy `defaultMode`/default-combo path (zero behaviour + * change for installs that predate the panel). Set by `getCompressionSettings`. + */ + enginesExplicit?: boolean; } export interface CompressionStats { @@ -193,6 +218,8 @@ export const DEFAULT_COMPRESSION_CONFIG: CompressionConfig = { { engine: "rtk", intensity: "standard" }, { engine: "caveman", intensity: "full" }, ], + engines: Object.fromEntries(ENGINE_IDS.map((id) => [id, { enabled: false }])), + activeComboId: null, }; export const DEFAULT_CAVEMAN_CONFIG: CavemanConfig = { diff --git a/src/app/(dashboard)/dashboard/context/caveman/CavemanContextPageClient.tsx b/src/app/(dashboard)/dashboard/context/caveman/CavemanContextPageClient.tsx index 0bb8d85086..3c2353e94e 100644 --- a/src/app/(dashboard)/dashboard/context/caveman/CavemanContextPageClient.tsx +++ b/src/app/(dashboard)/dashboard/context/caveman/CavemanContextPageClient.tsx @@ -84,10 +84,6 @@ export default function CavemanContextPageClient() { intensity: "lite", autoClarity: true, }; - const inputMode: InputModeConfig = { - enabled: settings?.cavemanConfig?.enabled ?? false, - intensity: (settings?.cavemanConfig?.intensity as InputModeConfig["intensity"]) ?? "lite", - }; const masterEnabled = settings?.enabled ?? false; const saveSettings = async (patch: Partial) => { @@ -108,11 +104,6 @@ export default function CavemanContextPageClient() { saveSettings({ languageConfig: { ...languageConfig, ...patch } }); }; - const updateInputMode = (patch: Partial) => { - const current = settings?.cavemanConfig ?? {}; - saveSettings({ cavemanConfig: { ...current, ...inputMode, ...patch } }); - }; - const updateOutputMode = (patch: Partial) => { saveSettings({ cavemanOutputMode: { ...outputMode, ...patch } }); }; @@ -232,34 +223,6 @@ export default function CavemanContextPageClient() { )} -
-

{t("inputCompressionTitle")}

-

{t("inputCompressionDesc")}

-
- - -
-
-

{t("analyticsTitle")}

@@ -282,16 +245,9 @@ export default function CavemanContextPageClient() {

{t("outputModeTitle")}

{t("outputModeDesc")}

+ {/* On/off + intensity for caveman output mode live in the panel + (/dashboard/context/settings). This page keeps the detailed knobs only. */}
- -
             {previewPrompt}
diff --git a/src/app/(dashboard)/dashboard/context/combos/CompressionHub.tsx b/src/app/(dashboard)/dashboard/context/combos/CompressionHub.tsx
index 1c19b52372..882e685969 100644
--- a/src/app/(dashboard)/dashboard/context/combos/CompressionHub.tsx
+++ b/src/app/(dashboard)/dashboard/context/combos/CompressionHub.tsx
@@ -172,64 +172,10 @@ export default function CompressionHub() {
     [settings]
   );
 
-  // ── Toggle a layer (enable/disable) ───────────────────────────────────────────
-  // Routed through the dedicated `/default` endpoint (setEngineInDefaultCombo): it
-  // accepts an empty pipeline (disabling the last layer) and inserts at the
-  // stackPriority-correct position — the [id] route requires `pipeline.min(1)`.
-  const toggleEngine = useCallback(
-    async (engineId: string) => {
-      if (!combo) return;
-      const existingIndex = combo.pipeline.findIndex((s) => s.engine === engineId);
-      const existingStep = existingIndex >= 0 ? combo.pipeline[existingIndex] : null;
-      const enabledNow = Boolean(existingStep && existingStep.config?.enabled !== false);
-      const prev = combo;
-
-      // Optimistic update (mirrors the server's insert-at-priority / remove logic).
-      let optimistic: PipelineStep[];
-      if (enabledNow) {
-        optimistic = combo.pipeline.filter((s) => s.engine !== engineId);
-      } else if (existingStep) {
-        optimistic = combo.pipeline.map((step, index) =>
-          index === existingIndex
-            ? { ...step, config: { ...(step.config ?? {}), enabled: true } }
-            : step
-        );
-      } else {
-        const priorityOf = (eid: string) => engines.find((e) => e.id === eid)?.stackPriority ?? 50;
-        optimistic = [...combo.pipeline];
-        let insertAt = optimistic.findIndex((s) => priorityOf(s.engine) > priorityOf(engineId));
-        if (insertAt < 0) insertAt = optimistic.length;
-        optimistic.splice(insertAt, 0, { engine: engineId });
-      }
-      setCombo({ ...combo, pipeline: optimistic });
-      setError(null);
-
-      try {
-        const res = await fetch("/api/context/combos/default", {
-          method: "PUT",
-          headers: { "Content-Type": "application/json" },
-          body: JSON.stringify({
-            engineId,
-            enabled: !enabledNow,
-            config: { ...(existingStep?.config ?? {}), enabled: !enabledNow },
-          }),
-        });
-        if (!res.ok) {
-          setCombo(prev);
-          setError("Failed to update layer.");
-          return;
-        }
-        const updated = await res.json();
-        if (Array.isArray(updated?.pipeline)) {
-          setCombo({ ...prev, pipeline: updated.pipeline });
-        }
-      } catch {
-        setCombo(prev);
-        setError("Failed to update layer.");
-      }
-    },
-    [combo, engines]
-  );
+  // Layer enable/disable moved to the single-source panel (/dashboard/context/settings,
+  // the `engines` map). The old per-layer toggle here wrote the now-deprecated
+  // /api/context/combos/default route (a 410 shim) — it has been removed. This Hub keeps
+  // the read-only derived view + the reorder control (which uses the named-combo [id] route).
 
   // ── Reorder an active layer ───────────────────────────────────────────────────
   // Persisted via the [id] route so the custom order survives (the `/default` route
@@ -423,9 +369,16 @@ export default function CompressionHub() {
           
           {activeSteps.length} layer(s)
         
+

+ Turn layers on/off and set their level in{" "} + + Compression Settings + + . You can reorder the active layers here. +

{activeSteps.length === 0 ? (

- No active layers. Enable a layer below to build the pipeline. + No active layers. Enable a layer in Compression Settings to build the pipeline.

) : (
    @@ -485,11 +438,6 @@ export default function CompressionHub() { > settings - toggleEngine(step.engine)} - ariaLabel={`Disable ${engine?.name ?? step.engine}`} - /> ); })} @@ -534,11 +482,6 @@ export default function CompressionHub() { > settings - toggleEngine(engine.id)} - ariaLabel={`Enable ${engine.name}`} - /> ))}
diff --git a/src/app/(dashboard)/dashboard/context/rtk/RtkContextPageClient.tsx b/src/app/(dashboard)/dashboard/context/rtk/RtkContextPageClient.tsx index 65f9bdc4ba..13df110d24 100644 --- a/src/app/(dashboard)/dashboard/context/rtk/RtkContextPageClient.tsx +++ b/src/app/(dashboard)/dashboard/context/rtk/RtkContextPageClient.tsx @@ -188,31 +188,9 @@ export default function RtkContextPageClient() { {config && (
-
- - + {/* On/off + intensity now live in the panel (/dashboard/context/settings). This + page edits RTK's detailed configuration only. */} +
- + {config.enabled && (
@@ -529,27 +486,9 @@ export default function CompressionSettingsTab() { /> - + {/* Caveman intensity (level) is set in the panel + (/dashboard/context/settings); kept out of this tab to avoid a + duplicate level control. */}

{t("compressionSkipRules")}

@@ -602,58 +541,16 @@ export default function CompressionSettingsTab() { {config.enabled && config.cavemanOutputMode && (
-
-
-

- {t("compressionSettingsCavemanOutputMode")} -

-

- Injects terse response instructions without rewriting provider output. -

-
- +
+

+ {t("compressionSettingsCavemanOutputMode")} +

+

+ Injects terse response instructions without rewriting provider output. Its on/off + and level are set in the panel (/dashboard/context/settings). +

- -