From 1cafd328c73d9d51dc6b65959e3361d5f4323b69 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Fri, 24 Jul 2026 20:37:37 -0300 Subject: [PATCH] fix(dashboard): persist compression engine detail settings (Headroom / session dedup / CCR) instead of dropping them on save (#8388) (#8404) --- .../fixes/8388-compression-detail-persist.md | 1 + config/quality/file-size-baseline.json | 3 +- .../services/compression/stepDetailConfig.ts | 26 +++++++ .../services/compression/strategySelector.ts | 12 ++-- open-sse/services/compression/types.ts | 37 ++++++++++ src/lib/db/compression.ts | 6 ++ src/lib/db/compressionDetailNormalizers.ts | 70 +++++++++++++++++++ .../compression/EngineConfigPage.tsx | 11 +-- .../validation/compressionConfigSchemas.ts | 20 ++++++ .../8388-compression-detail-persist.test.ts | 64 +++++++++++++++++ 10 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 changelog.d/fixes/8388-compression-detail-persist.md create mode 100644 open-sse/services/compression/stepDetailConfig.ts create mode 100644 src/lib/db/compressionDetailNormalizers.ts create mode 100644 tests/unit/8388-compression-detail-persist.test.ts diff --git a/changelog.d/fixes/8388-compression-detail-persist.md b/changelog.d/fixes/8388-compression-detail-persist.md new file mode 100644 index 0000000000..da963af987 --- /dev/null +++ b/changelog.d/fixes/8388-compression-detail-persist.md @@ -0,0 +1 @@ +- fix(dashboard): persist compression engine detail settings (Headroom / session dedup / CCR) instead of dropping them on save (#8388) diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 0ff32a4ea2..f2901f608d 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -186,7 +186,8 @@ "open-sse/handlers/videoGeneration.ts": 1275, "_rebaseline_2026_07_22_8010_codex_responses_engine": "PR #8010 (@JxnLexn) own growth: open-sse/mcp-server/schemas/tools.ts 1497->1505 (+8 = threading the new \"codex-responses\" literal into the compressionConfigureInput strategy/autoTriggerMode Zod enums and setCompressionEngineInput engine enum, mirroring the existing rtk/omniglyph enum entries; no new tool). open-sse/services/compression/strategySelector.ts 1043->1054 (+11 = one new `if (mode === \"codex-responses\")` dispatch branch in runCompression that delegates 100% to the new codexResponsesEngine.apply, mirroring the existing rtk single-mode dispatch, plus threading config.codexResponsesConfig.preserveToolNames into the shared adaptBodyForCompression call at the 3 existing call sites). src/lib/db/compression.ts (untracked, new-file cap 800) 794->845 (+51 = normalizeCodexResponsesConfig, mirroring the existing normalizeRtkConfig normalizer, plus registering \"codex-responses\" in the COMPRESSION_MODES/STACKED_PIPELINE_ENGINE_IDS/SINGLE_MODE_ENGINE sets and the getCompressionSettings load/save switch) — added to the baseline at its current size. All three are cohesive dispatch/normalizer wiring at existing chokepoints (mirroring the prior compression-mode rebaselines #6534/#6556), not extractable without hiding the mode-dispatch boundary. Covered by tests/unit/compression/codex-responses.test.ts (6) + omniglyph-registries.test.ts/types.test.ts (22, updated for the new mode).", "_rebaseline_2026_07_22_8034_compression_exclusions_persistence": "#8034 (compression exclusions) own growth: src/lib/db/compression.ts 845->850 (+5 = threading the new compressionExclusions field through the existing getCompressionSettings/saveCompressionSettings load/save switch over the shared key_value compression namespace — no new table, no raw SQL). Mirrors the prior compression-field rebaselines (#8010 codex-responses normalizer at the same chokepoint); the load/save switch is a single dispatch boundary, not extractable without hiding it. Covered by the PR's 8 node:test + 3 vitest cases.", - "src/lib/db/compression.ts": 866, + "_rebaseline_2026_07_24_8388_compression_detail_persist": "#8388 (compression engine DETAIL settings — Headroom/session-dedup/CCR — dropped on save) own growth: src/lib/db/compression.ts 866->872 (+6 = irreducible call-site wiring at the existing getCompressionSettings/updateCompressionSettings chokepoint: one import line, one `...buildDetailConfigDefaults()` spread in the seed config, and one `case \"sessionDedup\": case \"ccr\": applyDetailConfigUpdate(config, key, parsed); break;` load-switch case, mirroring the existing headroom/#8056 case immediately above it). The actual normalizer logic (normalizeSessionDedupConfig/normalizeCcrConfig, matching SESSION_DEDUP_SCHEMA/CCR_SCHEMA bounds) was EXTRACTED into a new leaf src/lib/db/compressionDetailNormalizers.ts (well under cap) so this frozen file only carries the minimal dispatch wiring. Covered by tests/unit/8388-compression-detail-persist.test.ts (schema-accept + full DB save->reload round-trip for both new sub-objects, plus a no-regression assertion on the existing headroom round-trip).", + "src/lib/db/compression.ts": 872, "open-sse/mcp-server/schemas/tools.ts": 1505, "open-sse/mcp-server/server.ts": 1555, "open-sse/mcp-server/tools/advancedTools.ts": 1120, diff --git a/open-sse/services/compression/stepDetailConfig.ts b/open-sse/services/compression/stepDetailConfig.ts new file mode 100644 index 0000000000..d1ba993641 --- /dev/null +++ b/open-sse/services/compression/stepDetailConfig.ts @@ -0,0 +1,26 @@ +// Resolves the persisted per-engine DETAIL sub-object (settings.headroom / .sessionDedup / +// .ccr) for a stacked-pipeline step. Extracted out of strategySelector.ts (frozen at cap by +// file-size-baseline.json — see scripts/check/check-file-size.mjs) rather than growing that +// file inline. +// +// #8056 wired settings.headroom.minRows into buildStepOptions so the dashboard value takes +// effect even when the stacked-pipeline step itself carries no config. #8388 extends the same +// merge to session-dedup and ccr, whose detail settings previously had nowhere to persist to +// (see compressionDetailNormalizers.ts on the DB write side of the same gap). +import type { CompressionConfig, CompressionPipelineStep } from "./types.ts"; + +export function resolveStepDetailConfig( + engine: CompressionPipelineStep["engine"], + config: CompressionConfig | undefined +): Record { + switch (engine) { + case "headroom": + return (config?.headroom as Record | undefined) ?? {}; + case "session-dedup": + return (config?.sessionDedup as Record | undefined) ?? {}; + case "ccr": + return (config?.ccr as Record | undefined) ?? {}; + default: + return {}; + } +} diff --git a/open-sse/services/compression/strategySelector.ts b/open-sse/services/compression/strategySelector.ts index 38c2779777..8785ddb550 100644 --- a/open-sse/services/compression/strategySelector.ts +++ b/open-sse/services/compression/strategySelector.ts @@ -28,6 +28,7 @@ import { decideStep, mergeStackStep, } from "./stackedStepCore.ts"; +import { resolveStepDetailConfig } from "./stepDetailConfig.ts"; import { registerBuiltinCompressionEngines } from "./engines/index.ts"; import { getCompressionEngine, getEngineEntry } from "./engines/registry.ts"; import { codexResponsesEngine } from "./engines/codexResponses/index.ts"; @@ -736,13 +737,12 @@ function buildStepOptions( step: CompressionPipelineStep, options?: StackOptions ): CompressionEngineApplyOptions { - // Headroom detail (minRows) lives on settings.headroom, not only on step.config. - // Merge it so the stacked runner honors the dashboard value (#8056). Explicit - // step.config still wins so combo pipelines can override per step. - const headroomDetail = - step.engine === "headroom" ? (options?.config?.headroom ?? {}) : {}; + // Detail sub-objects (headroom.minRows #8056; sessionDedup/ccr #8388) live on + // settings., not only on step.config. Merge them so the stacked runner + // honors the dashboard value. Explicit step.config still wins so combo pipelines + // can override per step. See resolveStepDetailConfig (stepDetailConfig.ts). const stepConfig: Record = { - ...headroomDetail, + ...resolveStepDetailConfig(step.engine, options?.config), ...(step.config ?? {}), ...(step.intensity ? { intensity: step.intensity } : {}), }; diff --git a/open-sse/services/compression/types.ts b/open-sse/services/compression/types.ts index 66e318371d..665af5988f 100644 --- a/open-sse/services/compression/types.ts +++ b/open-sse/services/compression/types.ts @@ -220,6 +220,10 @@ export interface CompressionConfig { ultra?: UltraConfig; /** Headroom SmartCrusher detail settings (minRows gate). */ headroom?: HeadroomConfig; + /** Session Dedup detail settings (minBlockChars / fuzzy, #8388). */ + sessionDedup?: SessionDedupConfig; + /** CCR (context-cache-retrieval) detail settings (minChars / retrievalRampFactor, #8388). */ + ccr?: CcrConfig; /** Provider-delegated context editing (Claude/Anthropic only). */ contextEditing?: ContextEditingConfig; /** Opt-in cache-aligned live-zone compression (default disabled). */ @@ -563,6 +567,39 @@ export const DEFAULT_HEADROOM_CONFIG: HeadroomConfig = { minRows: 8, }; +// ─── Session Dedup detail settings ─────────────────────────────────────────── +// Persisted under compression settings key `sessionDedup` (#8388 — was previously +// rendered on the detail page but had no sub-object to save into). + +/** Configuration for the Session Dedup engine detail page. */ +export interface SessionDedupConfig { + /** Minimum character count for a suffix block to be a dedup candidate. Matches DEFAULT_MIN_BLOCK_CHARS=80. */ + minBlockChars: number; + /** Opt-in fuzzy near-duplicate dedup (replaces ~85%+ similar messages with a CCR marker). */ + fuzzy: boolean; +} + +export const DEFAULT_SESSION_DEDUP_CONFIG: SessionDedupConfig = { + minBlockChars: 80, + fuzzy: false, +}; + +// ─── CCR (context-cache-retrieval) detail settings ─────────────────────────── +// Persisted under compression settings key `ccr` (#8388 — same gap as session-dedup). + +/** Configuration for the CCR engine detail page. */ +export interface CcrConfig { + /** Minimum character count for a block to be a CCR candidate. Matches DEFAULT_MIN_CHARS=600. */ + minChars: number; + /** How steeply frequently-retrieved blocks resist compression; 1 disables the ramp. */ + retrievalRampFactor: number; +} + +export const DEFAULT_CCR_CONFIG: CcrConfig = { + minChars: 600, + retrievalRampFactor: 2, +}; + export type { McpAccessibilityConfig } from "./engines/mcpAccessibility/constants.ts"; export { DEFAULT_MCP_ACCESSIBILITY_CONFIG, diff --git a/src/lib/db/compression.ts b/src/lib/db/compression.ts index 3b39ccd9e4..7c9b46732c 100644 --- a/src/lib/db/compression.ts +++ b/src/lib/db/compression.ts @@ -40,6 +40,7 @@ import { normalizePreserveSystemPromptMode, } from "@omniroute/open-sse/services/compression/preserveSystemPromptMode.ts"; import { maybePrewarmUltraSlmOnConfig } from "@omniroute/open-sse/services/compression/ultra.ts"; +import { applyDetailConfigUpdate, buildDetailConfigDefaults } from "./compressionDetailNormalizers"; const NAMESPACE = "compression"; const COMPRESSION_MODES = new Set([ @@ -612,6 +613,7 @@ export async function getCompressionSettings(): Promise { aggressive: normalizeAggressiveConfig(undefined), ultra: normalizeUltraConfig(undefined), headroom: normalizeHeadroomConfig(undefined), + ...buildDetailConfigDefaults(), contextBudget: normalizeContextBudgetConfig(undefined), contextEditing: { ...DEFAULT_CONTEXT_EDITING_CONFIG }, liveZone: { enabled: false }, @@ -724,6 +726,10 @@ export async function getCompressionSettings(): Promise { case "headroomConfig": config.headroom = normalizeHeadroomConfig(parsed); break; + case "sessionDedup": + case "ccr": + applyDetailConfigUpdate(config, key, parsed); + break; case "contextBudget": config.contextBudget = normalizeContextBudgetConfig(parsed); break; diff --git a/src/lib/db/compressionDetailNormalizers.ts b/src/lib/db/compressionDetailNormalizers.ts new file mode 100644 index 0000000000..6da34a9153 --- /dev/null +++ b/src/lib/db/compressionDetailNormalizers.ts @@ -0,0 +1,70 @@ +// Normalizers for the compression engine DETAIL settings sub-objects that persist to a +// single key_value row each (settings.sessionDedup / settings.ccr). Extracted out of +// src/lib/db/compression.ts (frozen at cap by file-size-baseline.json — see +// scripts/check/check-file-size.mjs) rather than growing that file inline. +// +// #8388: session-dedup and ccr detail fields (minBlockChars/fuzzy, minChars/ +// retrievalRampFactor) were editable on the EngineConfigPage detail form but had no +// persisted sub-object — mirrors the #8056 headroom/minRows fix (normalizeHeadroomConfig +// in compression.ts), extended to the two engines #8056 left uncovered. +import { + DEFAULT_CCR_CONFIG, + DEFAULT_SESSION_DEDUP_CONFIG, + type CcrConfig, + type CompressionConfig, + type SessionDedupConfig, +} from "@omniroute/open-sse/services/compression/types.ts"; + +function toRecord(value: unknown): Record { + return value && typeof value === "object" ? (value as Record) : {}; +} + +function boundedInt(value: unknown, fallback: number, min: number, max: number): number { + if (typeof value !== "number" || !Number.isFinite(value)) return fallback; + return Math.min(max, Math.max(min, Math.floor(value))); +} + +/** Matches SESSION_DEDUP_SCHEMA bounds (engines/session-dedup/index.ts). */ +export function normalizeSessionDedupConfig(value: unknown): SessionDedupConfig { + const record = toRecord(value); + return { + ...DEFAULT_SESSION_DEDUP_CONFIG, + minBlockChars: boundedInt( + record.minBlockChars, + DEFAULT_SESSION_DEDUP_CONFIG.minBlockChars, + 1, + 100000 + ), + fuzzy: typeof record.fuzzy === "boolean" ? record.fuzzy : DEFAULT_SESSION_DEDUP_CONFIG.fuzzy, + }; +} + +/** Matches CCR_SCHEMA bounds (engines/ccr/index.ts). */ +export function normalizeCcrConfig(value: unknown): CcrConfig { + const record = toRecord(value); + return { + ...DEFAULT_CCR_CONFIG, + minChars: boundedInt(record.minChars, DEFAULT_CCR_CONFIG.minChars, 100, 1_000_000), + retrievalRampFactor: boundedInt( + record.retrievalRampFactor, + DEFAULT_CCR_CONFIG.retrievalRampFactor, + 1, + 100 + ), + }; +} + +/** Default sub-objects spread into getCompressionSettings' seed config. */ +export function buildDetailConfigDefaults(): Pick { + return { sessionDedup: normalizeSessionDedupConfig(undefined), ccr: normalizeCcrConfig(undefined) }; +} + +/** Applies a stored sessionDedup/ccr row onto config during getCompressionSettings' row scan. */ +export function applyDetailConfigUpdate( + config: CompressionConfig, + key: "sessionDedup" | "ccr", + parsed: unknown +): void { + if (key === "sessionDedup") config.sessionDedup = normalizeSessionDedupConfig(parsed); + else config.ccr = normalizeCcrConfig(parsed); +} diff --git a/src/shared/components/compression/EngineConfigPage.tsx b/src/shared/components/compression/EngineConfigPage.tsx index 79b5859ae5..5c1981a4ec 100644 --- a/src/shared/components/compression/EngineConfigPage.tsx +++ b/src/shared/components/compression/EngineConfigPage.tsx @@ -21,14 +21,17 @@ interface EngineEntry { // Engines whose detailed config has a dedicated sub-object in the compression // settings store. The on/off + level for ALL engines now live in the panel // (/dashboard/context/settings, the `engines` map); only these have a place to -// persist the extra per-engine fields edited on this page. Other structural -// engines (lite, session-dedup, ccr, llmlingua, relevance) still have no -// dedicated sub-object — their page keeps the detail form + preview but has -// nothing extra to persist yet. +// persist the extra per-engine fields edited on this page. session-dedup and ccr +// joined headroom in #8388 (they previously rendered a real, editable detail form +// with no Save affordance — edits vanished on reload). Other structural engines +// (lite, llmlingua, relevance) still have no dedicated sub-object — their page +// keeps the detail form + preview but has nothing extra to persist yet. const SETTINGS_SUBOBJECT: Record = { aggressive: "aggressive", ultra: "ultra", headroom: "headroom", + "session-dedup": "sessionDedup", + ccr: "ccr", }; interface CompressionSettings { diff --git a/src/shared/validation/compressionConfigSchemas.ts b/src/shared/validation/compressionConfigSchemas.ts index 2b26585f97..74c56d275a 100644 --- a/src/shared/validation/compressionConfigSchemas.ts +++ b/src/shared/validation/compressionConfigSchemas.ts @@ -161,6 +161,24 @@ export const headroomConfigSchema = z }) .strict(); +// Session Dedup / CCR detail settings (#8388 — sibling gap to headroom/#8056: the +// EngineConfigPage detail form was renderable but PUT bodies had no slot to persist +// into). Ranges mirror SESSION_DEDUP_SCHEMA / CCR_SCHEMA (engines/session-dedup, +// engines/ccr) so the validation layer stays in lockstep with the engine's own bounds. +export const sessionDedupConfigSchema = z + .object({ + minBlockChars: z.number().int().min(1).max(100000).optional(), + fuzzy: z.boolean().optional(), + }) + .strict(); + +export const ccrConfigSchema = z + .object({ + minChars: z.number().int().min(100).max(1_000_000).optional(), + retrievalRampFactor: z.number().min(1).max(100).optional(), + }) + .strict(); + const noConfigSchema = z.object({}).strict(); // Structural engines (session-dedup / ccr / headroom / relevance / llmlingua) do not @@ -344,6 +362,8 @@ export const compressionSettingsUpdateSchema = z aggressive: aggressiveConfigSchema.optional(), ultra: ultraConfigSchema.optional(), headroom: headroomConfigSchema.optional(), + sessionDedup: sessionDedupConfigSchema.optional(), + ccr: ccrConfigSchema.optional(), contextBudget: contextBudgetConfigSchema.optional(), contextEditing: contextEditingConfigSchema.optional(), liveZone: z.object({ enabled: z.boolean() }).strict().optional(), diff --git a/tests/unit/8388-compression-detail-persist.test.ts b/tests/unit/8388-compression-detail-persist.test.ts new file mode 100644 index 0000000000..c65d29ba95 --- /dev/null +++ b/tests/unit/8388-compression-detail-persist.test.ts @@ -0,0 +1,64 @@ +// #8388 — Compression engine DETAIL settings (Headroom / session dedup / CCR) do not +// persist on save. Root cause was a two-layer gap on origin/release/v3.8.49: +// (1) the .strict() Zod schema (compressionSettingsUpdateSchema) had no `sessionDedup` +// / `ccr` top-level keys, so a PUT body carrying either was rejected outright; +// (2) even past validation, src/lib/db/compression.ts had no normalizer/switch-case +// wired for those two sub-objects (only `headroom` got the #8056 treatment), so a +// set → save → reload round-trip would silently drop the values. +// This test asserts the FULL round-trip end-to-end (schema parse -> DB write -> DB read), +// not just schema-level parsing, per the plan-file's explicit instruction. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +// Isolate DATA_DIR so this test never touches a real installed DB (see MEMORY: "teste sem +// isolateDataDir → DB REAL"). Must be set BEFORE importing anything that resolves getDbInstance(). +const tmpDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-8388-")); +process.env.DATA_DIR = tmpDataDir; + +const { compressionSettingsUpdateSchema } = await import( + "../../src/shared/validation/compressionConfigSchemas.ts" +); +const { resetDbInstance } = await import("../../src/lib/db/core.ts"); +const { getCompressionSettings, updateCompressionSettings } = await import( + "../../src/lib/db/compression.ts" +); + +test.after(() => { + resetDbInstance(); + fs.rmSync(tmpDataDir, { recursive: true, force: true }); +}); + +test("#8388: PUT body carrying ccr detail (minChars/retrievalRampFactor) is ACCEPTED by the schema", () => { + const parsed = compressionSettingsUpdateSchema.safeParse({ + ccr: { minChars: 5000, retrievalRampFactor: 10 }, + }); + assert.equal(parsed.success, true); +}); + +test("#8388: PUT body carrying session-dedup detail (minBlockChars/fuzzy) is ACCEPTED by the schema", () => { + const parsed = compressionSettingsUpdateSchema.safeParse({ + sessionDedup: { minBlockChars: 200, fuzzy: true }, + }); + assert.equal(parsed.success, true); +}); + +test("#8388: session-dedup detail round-trips through save -> reload (DB layer)", async () => { + await updateCompressionSettings({ sessionDedup: { minBlockChars: 321, fuzzy: true } }); + const reloaded = await getCompressionSettings(); + assert.deepEqual(reloaded.sessionDedup, { minBlockChars: 321, fuzzy: true }); +}); + +test("#8388: ccr detail round-trips through save -> reload (DB layer)", async () => { + await updateCompressionSettings({ ccr: { minChars: 4242, retrievalRampFactor: 7 } }); + const reloaded = await getCompressionSettings(); + assert.deepEqual(reloaded.ccr, { minChars: 4242, retrievalRampFactor: 7 }); +}); + +test("#8388: headroom minRows STILL round-trips (proves #8056 fix stays intact, no regression)", async () => { + await updateCompressionSettings({ headroom: { minRows: 5 } }); + const reloaded = await getCompressionSettings(); + assert.deepEqual(reloaded.headroom, { minRows: 5 }); +});