From 72e223b9a93f6392f9a9175a5877bc652afca32a Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Tue, 30 Jun 2026 11:00:55 -0300 Subject: [PATCH] =?UTF-8?q?feat(compression):=20T05/C5=20=E2=80=94=20prese?= =?UTF-8?q?rveSystemPrompt=20mode=20enum=20(always|whenNoCache|never)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the implicit 3-state behaviour of the boolean preserveSystemPrompt with an explicit authoritative enum, keeping the boolean as the engine-facing effective value: - always -> never compress the system prompt (legacy true). - whenNoCache -> compress it only when there is no cache to protect (legacy false; the #3890/#3955 cache guard already did exactly this). - never -> always compress the system prompt, even when it breaks a prompt cache (new). The mode is resolved to the effective boolean at resolveCacheAwareConfig (already upstream in chatCore with the caching context), generalizing the previous hard-coded force-true. Back-compat shim derives the mode from the legacy boolean, so existing configs and the cache-guard tests are behaviour-identical. Persisted in compression settings, validated by Zod, exposed as a 3-way select in both compression panels. New helper open-sse/services/compression/preserveSystemPromptMode.ts is the single source of the enum<->boolean mapping. TDD: helper, cache-aware resolution incl. never, DB round-trip, UI select render + save. --- .../services/compression/cacheAwareConfig.ts | 35 +++++--- .../compression/preserveSystemPromptMode.ts | 72 +++++++++++++++++ open-sse/services/compression/types.ts | 22 +++++ .../context/settings/CompressionPanel.tsx | 29 ++++--- .../components/CompressionSettingsTab.tsx | 31 ++++--- src/i18n/messages/en.json | 3 + src/lib/db/compression.ts | 7 ++ .../validation/compressionConfigSchemas.ts | 1 + .../cache-aware-preserve-mode.test.ts | 81 +++++++++++++++++++ tests/unit/compression/db.test.ts | 20 +++++ .../preserve-system-prompt-mode.test.ts | 78 ++++++++++++++++++ ...ession-settings-tab-consolidation.test.tsx | 30 +++++++ 12 files changed, 378 insertions(+), 31 deletions(-) create mode 100644 open-sse/services/compression/preserveSystemPromptMode.ts create mode 100644 tests/unit/compression/cache-aware-preserve-mode.test.ts create mode 100644 tests/unit/compression/preserve-system-prompt-mode.test.ts diff --git a/open-sse/services/compression/cacheAwareConfig.ts b/open-sse/services/compression/cacheAwareConfig.ts index 5f0c01fb71..e6d1f2d447 100644 --- a/open-sse/services/compression/cacheAwareConfig.ts +++ b/open-sse/services/compression/cacheAwareConfig.ts @@ -1,23 +1,36 @@ import type { CompressionConfig } from "./types.ts"; import type { CachingDetectionContext } from "./cachingAware.ts"; import { detectCachingContext, getCacheAwareStrategy } from "./cachingAware.ts"; +import { + normalizePreserveSystemPromptMode, + resolvePreserveSystemPrompt, +} from "./preserveSystemPromptMode.ts"; /** - * #3890: honor the cache-aware `skipSystemPrompt` decision that - * `getCacheAwareStrategy` already computes but `selectCompressionStrategy` - * cannot return. In a caching context the system prompt is part of the - * cacheable prefix, so compressing it breaks the upstream prompt cache. + * #3890/#3955 + T05/C5: materialize the engine-facing `preserveSystemPrompt` + * boolean from the authoritative `preserveSystemPromptMode` intent, using the + * cache-aware `skipSystemPrompt` signal that `getCacheAwareStrategy` already + * computes (a caching provider — or `cache_control` — means the system prompt is + * part of the cacheable prefix, so compressing it breaks the upstream cache). + * + * This generalizes the previous hard-coded "force `true` when a cache is present + * and the operator disabled preservation" into the three modes: + * - `always` → always `true`. + * - `whenNoCache` → `true` only when a cache is present (the legacy `false` + * behaviour — preserved exactly for back-compat). + * - `never` → always `false`, even when it breaks a prompt cache. */ export function resolveCacheAwareConfig( config: CompressionConfig, body?: Record, context?: CachingDetectionContext ): CompressionConfig { - if (!body) return config; - const ctx = detectCachingContext(body, context); - const cacheAware = getCacheAwareStrategy(config.defaultMode, ctx); - if (cacheAware.skipSystemPrompt && config.preserveSystemPrompt === false) { - return { ...config, preserveSystemPrompt: true }; - } - return config; + const mode = normalizePreserveSystemPromptMode(config); + // No request body → no cacheable prefix to detect; honor the mode at its no-cache baseline. + const hasCache = body + ? getCacheAwareStrategy(config.defaultMode, detectCachingContext(body, context)).skipSystemPrompt + : false; + const effective = resolvePreserveSystemPrompt(mode, { hasCache }); + if (effective === config.preserveSystemPrompt) return config; + return { ...config, preserveSystemPrompt: effective }; } diff --git a/open-sse/services/compression/preserveSystemPromptMode.ts b/open-sse/services/compression/preserveSystemPromptMode.ts new file mode 100644 index 0000000000..39528d442a --- /dev/null +++ b/open-sse/services/compression/preserveSystemPromptMode.ts @@ -0,0 +1,72 @@ +import type { CompressionConfig, PreserveSystemPromptMode } from "./types.ts"; + +/** + * T05/C5 — system-prompt preservation mode. + * + * The engine-facing field is the boolean `CompressionConfig.preserveSystemPrompt` + * (truthy = skip/preserve the system prompt). Its authoritative *intent* is the + * `preserveSystemPromptMode` enum, resolved to that boolean at the cache-aware layer + * (`resolveCacheAwareConfig`), which already runs upstream in `chatCore` with the + * caching context. This module is the single source of the enum<->boolean mapping so + * the legacy boolean and the new enum can never drift. + * + * Mode semantics: + * - `always` → preserve the system prompt unconditionally. + * - `whenNoCache` → preserve it only when there is a cache to protect (provider caches + * the prefix or `cache_control` is present); compress it otherwise. + * This is exactly what the legacy `preserveSystemPrompt: false` already + * did via the #3890/#3955 cache guard. + * - `never` → compress the system prompt even when it breaks a prompt cache. + */ +export const PRESERVE_SYSTEM_PROMPT_MODES: readonly PreserveSystemPromptMode[] = [ + "always", + "whenNoCache", + "never", +]; + +export function isPreserveSystemPromptMode(value: unknown): value is PreserveSystemPromptMode { + return ( + typeof value === "string" && + (PRESERVE_SYSTEM_PROMPT_MODES as readonly string[]).includes(value) + ); +} + +/** + * Back-compat shim: derive the authoritative mode from a config. An explicit + * `preserveSystemPromptMode` always wins; otherwise the legacy boolean is mapped + * 1:1 to the behaviour it already had (`false → whenNoCache`, anything else → `always`). + */ +export function normalizePreserveSystemPromptMode( + config: Pick +): PreserveSystemPromptMode { + if (isPreserveSystemPromptMode(config.preserveSystemPromptMode)) { + return config.preserveSystemPromptMode; + } + return config.preserveSystemPrompt === false ? "whenNoCache" : "always"; +} + +/** + * Resolve a mode to the effective engine-facing boolean given whether a cacheable + * prefix is present. `true` = preserve (skip), `false` = compress the system prompt. + */ +export function resolvePreserveSystemPrompt( + mode: PreserveSystemPromptMode, + { hasCache }: { hasCache: boolean } +): boolean { + switch (mode) { + case "always": + return true; + case "never": + return false; + case "whenNoCache": + return hasCache; + } +} + +/** + * The no-cache projection of a mode — the stored/effective boolean used as a sane + * default for any reader that runs before the cache-aware layer materializes it. + */ +export function modeToBaselineBoolean(mode: PreserveSystemPromptMode): boolean { + return resolvePreserveSystemPrompt(mode, { hasCache: false }); +} diff --git a/open-sse/services/compression/types.ts b/open-sse/services/compression/types.ts index 755d9256f1..8e2e9b2d7f 100644 --- a/open-sse/services/compression/types.ts +++ b/open-sse/services/compression/types.ts @@ -145,13 +145,34 @@ export interface EngineToggle { level?: string; } +/** T05/C5 — system-prompt preservation intent (see `CompressionConfig.preserveSystemPromptMode`). */ +export type PreserveSystemPromptMode = "always" | "whenNoCache" | "never"; + export interface CompressionConfig { enabled: boolean; defaultMode: CompressionMode; autoTriggerMode?: CompressionMode; autoTriggerTokens: number; cacheMinutes: number; + /** + * Effective, engine-facing boolean: when truthy the system prompt is skipped + * (preserved, not compressed). Kept as the materialized value all engines read. + * Its authoritative *intent* is `preserveSystemPromptMode` (T05/C5); this boolean + * is the no-cache projection of that mode, refined up to `true` by + * `resolveCacheAwareConfig` when a cacheable prefix is detected. + */ preserveSystemPrompt: boolean; + /** + * T05/C5 — authoritative system-prompt preservation intent: + * - `always`: never compress the system prompt. + * - `whenNoCache`: compress it only when there is no cache to protect + * (preserve when the provider caches or `cache_control` is present). This is the + * behaviour the legacy `preserveSystemPrompt: false` already had via the cache guard. + * - `never`: always compress the system prompt, even when it breaks a prompt cache. + * Optional/back-compat: absent → derived from the legacy boolean + * (`false → whenNoCache`, otherwise `always`). + */ + preserveSystemPromptMode?: PreserveSystemPromptMode; mcpDescriptionCompressionEnabled?: boolean; comboOverrides: Record; compressionComboId?: string | null; @@ -289,6 +310,7 @@ export const DEFAULT_COMPRESSION_CONFIG: CompressionConfig = { autoTriggerTokens: 0, cacheMinutes: 5, preserveSystemPrompt: true, + preserveSystemPromptMode: "always", mcpDescriptionCompressionEnabled: true, comboOverrides: {}, compressionComboId: null, diff --git a/src/app/(dashboard)/dashboard/context/settings/CompressionPanel.tsx b/src/app/(dashboard)/dashboard/context/settings/CompressionPanel.tsx index 192ba9f39b..da6a856b06 100644 --- a/src/app/(dashboard)/dashboard/context/settings/CompressionPanel.tsx +++ b/src/app/(dashboard)/dashboard/context/settings/CompressionPanel.tsx @@ -51,6 +51,7 @@ interface CompressionConfig { enabled: boolean; autoTriggerTokens: number; preserveSystemPrompt: boolean; + preserveSystemPromptMode?: "always" | "whenNoCache" | "never"; engines: Record; activeComboId: string | null; cavemanOutputMode?: CavemanOutputModeConfig; @@ -458,15 +459,25 @@ export default function CompressionPanel() { diff --git a/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx b/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx index a68426f21d..da4b993bd7 100644 --- a/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx @@ -65,6 +65,7 @@ interface CompressionConfig extends CompressionTokenSaverConfig { autoTriggerTokens: number; cacheMinutes: number; preserveSystemPrompt: boolean; + preserveSystemPromptMode?: "always" | "whenNoCache" | "never"; mcpDescriptionCompressionEnabled?: boolean; comboOverrides: Record; cavemanConfig?: CavemanConfig; @@ -372,18 +373,26 @@ export default function CompressionSettingsTab() {