From 072d224a2fe74410d21cd5a161109981b8ba2d0a Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 18 Apr 2026 10:28:39 -0400 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=90=9B=20fix:=20accept=20documented?= =?UTF-8?q?=20`summarization.trigger.type`=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Zod schema for `summarization.trigger.type` only accepted `'token_count'`, but: - the documentation lists `token_ratio`, `remaining_tokens`, and `messages_to_refine` as valid - the `@librechat/agents` runtime only evaluates those three types and silently no-ops on anything else The result was a double failure: any user following the docs hit a startup Zod error, and anyone who matched the schema by using `token_count` got a silent no-op at runtime where summarization never fired. Align the schema with the documented, runtime-supported trigger types. Closes #12721 --- .../__tests__/run-summarization.test.ts | 4 +- .../specs/config-schemas.spec.ts | 45 +++++++++++++++++++ packages/data-provider/src/config.ts | 2 +- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/packages/api/src/agents/__tests__/run-summarization.test.ts b/packages/api/src/agents/__tests__/run-summarization.test.ts index 2bc0da253a98..98a8ed87e16d 100644 --- a/packages/api/src/agents/__tests__/run-summarization.test.ts +++ b/packages/api/src/agents/__tests__/run-summarization.test.ts @@ -218,7 +218,7 @@ describe('summarizationConfig field passthrough', () => { const agents = await callAndCapture({ summarizationConfig: { enabled: true, - trigger: { type: 'token_count', value: 8000 }, + trigger: { type: 'token_ratio', value: 0.8 }, provider: 'anthropic', model: 'claude-3-haiku', parameters: { temperature: 0.2 }, @@ -233,7 +233,7 @@ describe('summarizationConfig field passthrough', () => { // `enabled` is not forwarded to the agent-level config — it is resolved // into the separate `summarizationEnabled` boolean on the agent input. expect(agents[0].summarizationEnabled).toBe(true); - expect(config.trigger).toEqual({ type: 'token_count', value: 8000 }); + expect(config.trigger).toEqual({ type: 'token_ratio', value: 0.8 }); expect(config.provider).toBe('anthropic'); expect(config.model).toBe('claude-3-haiku'); expect(config.parameters).toEqual({ temperature: 0.2 }); diff --git a/packages/data-provider/specs/config-schemas.spec.ts b/packages/data-provider/specs/config-schemas.spec.ts index 6e76bced06ac..833baea7c3d5 100644 --- a/packages/data-provider/specs/config-schemas.spec.ts +++ b/packages/data-provider/specs/config-schemas.spec.ts @@ -7,6 +7,8 @@ import { interfaceSchema, fileStorageSchema, fileStrategiesSchema, + summarizationTriggerSchema, + summarizationConfigSchema, } from '../src/config'; import { tModelSpecPresetSchema, EModelEndpoint } from '../src/schemas'; import { FileSources } from '../src/types/files'; @@ -502,3 +504,46 @@ describe('interfaceSchema', () => { expect(result.modelSelect).toBe(false); }); }); + +describe('summarizationTriggerSchema', () => { + it.each(['token_ratio', 'remaining_tokens', 'messages_to_refine'] as const)( + 'accepts documented trigger type "%s"', + (type) => { + const result = summarizationTriggerSchema.safeParse({ type, value: 0.8 }); + expect(result.success).toBe(true); + }, + ); + + it('rejects the legacy/typoed "token_count" trigger type', () => { + const result = summarizationTriggerSchema.safeParse({ + type: 'token_count', + value: 8000, + }); + expect(result.success).toBe(false); + }); + + it('rejects unknown trigger types', () => { + const result = summarizationTriggerSchema.safeParse({ + type: 'never_heard_of_it', + value: 1, + }); + expect(result.success).toBe(false); + }); + + it('rejects zero or negative values', () => { + expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: 0 }).success).toBe( + false, + ); + expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: -0.5 }).success).toBe( + false, + ); + }); + + it('parses inside the full summarization config', () => { + const result = summarizationConfigSchema.safeParse({ + enabled: true, + trigger: { type: 'token_ratio', value: 0.8 }, + }); + expect(result.success).toBe(true); + }); +}); diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index c0f614decd9b..5cc23ef43e79 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1021,7 +1021,7 @@ export const memorySchema = z.object({ export type TMemoryConfig = DeepPartial>; export const summarizationTriggerSchema = z.object({ - type: z.enum(['token_count']), + type: z.enum(['token_ratio', 'remaining_tokens', 'messages_to_refine']), value: z.number().positive(), }); From 599c0fed2b7eedf087f45ba02a0dcd959306dccc Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 18 Apr 2026 10:43:12 -0400 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=A7=B9=20fix:=20bound=20`token=5Frati?= =?UTF-8?q?o`=20trigger=20value=20to=20(0,=201]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Codex review: the previous schema accepted `value: z.number().positive()` for every trigger type. That meant `trigger: { type: 'token_ratio', value: 80 }` (presumably meant as "80%") passed validation and then silently never fired — because `usedRatio = 1 - remaining/max` is bounded at 1, so `>= 80` is always false. That is exactly the silent-no-op pattern this PR is trying to eliminate. Switch to a discriminated union so each trigger type has its own value constraint: - `token_ratio`: `(0, 1]` — documented as a fraction, so 80 is nonsense - `remaining_tokens`: positive — token counts can be large - `messages_to_refine`: positive — message counts can be > 1 Added tests for the upper-bound rejection and the inclusive upper bound (`value: 1` still accepted as a valid "fire at 100%" extreme). --- .../specs/config-schemas.spec.ts | 45 ++++++++++++++++--- packages/data-provider/src/config.ts | 18 ++++++-- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/packages/data-provider/specs/config-schemas.spec.ts b/packages/data-provider/specs/config-schemas.spec.ts index 833baea7c3d5..5b7085b91237 100644 --- a/packages/data-provider/specs/config-schemas.spec.ts +++ b/packages/data-provider/specs/config-schemas.spec.ts @@ -506,13 +506,14 @@ describe('interfaceSchema', () => { }); describe('summarizationTriggerSchema', () => { - it.each(['token_ratio', 'remaining_tokens', 'messages_to_refine'] as const)( - 'accepts documented trigger type "%s"', - (type) => { - const result = summarizationTriggerSchema.safeParse({ type, value: 0.8 }); - expect(result.success).toBe(true); - }, - ); + it.each([ + ['token_ratio', 0.8], + ['remaining_tokens', 500], + ['messages_to_refine', 4], + ] as const)('accepts documented trigger type "%s" with a sensible value', (type, value) => { + const result = summarizationTriggerSchema.safeParse({ type, value }); + expect(result.success).toBe(true); + }); it('rejects the legacy/typoed "token_count" trigger type', () => { const result = summarizationTriggerSchema.safeParse({ @@ -537,6 +538,36 @@ describe('summarizationTriggerSchema', () => { expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: -0.5 }).success).toBe( false, ); + expect( + summarizationTriggerSchema.safeParse({ type: 'remaining_tokens', value: 0 }).success, + ).toBe(false); + expect( + summarizationTriggerSchema.safeParse({ type: 'messages_to_refine', value: 0 }).success, + ).toBe(false); + }); + + it('rejects token_ratio values > 1 to catch the "80 meant as 80%" mistake', () => { + expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: 80 }).success).toBe( + false, + ); + expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: 1.01 }).success).toBe( + false, + ); + }); + + it('accepts token_ratio value at the upper bound of 1', () => { + expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: 1 }).success).toBe( + true, + ); + }); + + it('allows remaining_tokens and messages_to_refine values above 1 (token/message counts)', () => { + expect( + summarizationTriggerSchema.safeParse({ type: 'remaining_tokens', value: 2000 }).success, + ).toBe(true); + expect( + summarizationTriggerSchema.safeParse({ type: 'messages_to_refine', value: 20 }).success, + ).toBe(true); }); it('parses inside the full summarization config', () => { diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 5cc23ef43e79..bf24d126ff66 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1020,10 +1020,20 @@ export const memorySchema = z.object({ export type TMemoryConfig = DeepPartial>; -export const summarizationTriggerSchema = z.object({ - type: z.enum(['token_ratio', 'remaining_tokens', 'messages_to_refine']), - value: z.number().positive(), -}); +export const summarizationTriggerSchema = z.discriminatedUnion('type', [ + z.object({ + type: z.literal('token_ratio'), + value: z.number().positive().max(1), + }), + z.object({ + type: z.literal('remaining_tokens'), + value: z.number().positive(), + }), + z.object({ + type: z.literal('messages_to_refine'), + value: z.number().positive(), + }), +]); export const contextPruningSchema = z.object({ enabled: z.boolean().optional(), From bc4e2c89b02fe3a9b5958357b55bb921479a5f87 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 18 Apr 2026 10:52:37 -0400 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=A7=B9=20fix:=20accept=20`token=5Frat?= =?UTF-8?q?io:=200`=20per=20documented=200.0=E2=80=931.0=20inclusive=20ran?= =?UTF-8?q?ge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Codex review: `.positive()` rejected `value: 0`, but the docs describe the `token_ratio` range as `0.0–1.0` (both inclusive). Admins who copy the documented lower bound into their YAML would fail schema validation at startup. Switch `token_ratio` to `.min(0).max(1)`. `0` is a valid (if extreme) setting — the agents SDK's `usedRatio >= 0` check will fire as soon as there is anything to refine, which is a legitimate "always summarize when pruning happens" configuration. `remaining_tokens` and `messages_to_refine` keep `.positive()`: both are counts, and `0` there produces no meaningful behavior (the SDK has an early return for `messagesToRefineCount <= 0`). --- .../specs/config-schemas.spec.ts | 19 ++++++++++++++----- packages/data-provider/src/config.ts | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/data-provider/specs/config-schemas.spec.ts b/packages/data-provider/specs/config-schemas.spec.ts index 5b7085b91237..7c45ce812f14 100644 --- a/packages/data-provider/specs/config-schemas.spec.ts +++ b/packages/data-provider/specs/config-schemas.spec.ts @@ -531,13 +531,19 @@ describe('summarizationTriggerSchema', () => { expect(result.success).toBe(false); }); - it('rejects zero or negative values', () => { - expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: 0 }).success).toBe( - false, - ); + it('rejects negative values on any trigger type', () => { expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: -0.5 }).success).toBe( false, ); + expect( + summarizationTriggerSchema.safeParse({ type: 'remaining_tokens', value: -1 }).success, + ).toBe(false); + expect( + summarizationTriggerSchema.safeParse({ type: 'messages_to_refine', value: -1 }).success, + ).toBe(false); + }); + + it('rejects zero for count-based triggers where it has no meaningful effect', () => { expect( summarizationTriggerSchema.safeParse({ type: 'remaining_tokens', value: 0 }).success, ).toBe(false); @@ -555,7 +561,10 @@ describe('summarizationTriggerSchema', () => { ); }); - it('accepts token_ratio value at the upper bound of 1', () => { + it('accepts token_ratio values at the inclusive 0 and 1 bounds per docs', () => { + expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: 0 }).success).toBe( + true, + ); expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: 1 }).success).toBe( true, ); diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index bf24d126ff66..6db847e9763a 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1023,7 +1023,7 @@ export type TMemoryConfig = DeepPartial>; export const summarizationTriggerSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('token_ratio'), - value: z.number().positive().max(1), + value: z.number().min(0).max(1), }), z.object({ type: z.literal('remaining_tokens'), From 1aba3264e44aaa30205c4fc6fc56d32b7b80f8b9 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 18 Apr 2026 11:01:54 -0400 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=90=9B=20fix:=20preserve=20`token=5Fr?= =?UTF-8?q?atio`=20trigger=20when=20`value:=200`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Codex review: now that the schema accepts `token_ratio: 0`, `shapeSummarizationConfig` would silently drop it because of a truthy check on `config?.trigger?.value`. The trigger would disappear and the runtime would fall back to "no trigger configured" — which fires on any pruning rather than honoring the explicit ratio. Switch to `typeof value === 'number'`, which preserves `0` while still rejecting `undefined`/`null`. Added a regression test that asserts `{ type: 'token_ratio', value: 0 }` survives the shaping function untouched. --- .../src/agents/__tests__/run-summarization.test.ts | 11 +++++++++++ packages/api/src/agents/run.ts | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/api/src/agents/__tests__/run-summarization.test.ts b/packages/api/src/agents/__tests__/run-summarization.test.ts index 98a8ed87e16d..5699d2ad45a2 100644 --- a/packages/api/src/agents/__tests__/run-summarization.test.ts +++ b/packages/api/src/agents/__tests__/run-summarization.test.ts @@ -254,6 +254,17 @@ describe('summarizationConfig field passthrough', () => { expect(config.provider).toBe('openAI'); expect(config.model).toBe('gpt-4o'); }); + + it('preserves `token_ratio` trigger with `value: 0` (documented, extreme-but-valid)', async () => { + const agents = await callAndCapture({ + summarizationConfig: { + enabled: true, + trigger: { type: 'token_ratio', value: 0 }, + }, + }); + const config = agents[0].summarizationConfig as Record; + expect(config.trigger).toEqual({ type: 'token_ratio', value: 0 }); + }); }); // --------------------------------------------------------------------------- diff --git a/packages/api/src/agents/run.ts b/packages/api/src/agents/run.ts index b6b5e6a14d90..cb4eda576322 100644 --- a/packages/api/src/agents/run.ts +++ b/packages/api/src/agents/run.ts @@ -195,7 +195,7 @@ function shapeSummarizationConfig( const provider = config?.provider ?? fallbackProvider; const model = config?.model ?? fallbackModel; const trigger = - config?.trigger?.type && config?.trigger?.value + config?.trigger?.type && typeof config?.trigger?.value === 'number' ? { type: config.trigger.type, value: config.trigger.value } : undefined; From a2731a9e1a71c78135af2608d730d51b54fe2b34 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 18 Apr 2026 11:08:21 -0400 Subject: [PATCH 5/8] =?UTF-8?q?=F0=9F=A7=B9=20fix:=20reject=20non-finite?= =?UTF-8?q?=20trigger=20values=20at=20schema=20level?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Codex review: `z.number().positive()` still accepts `Infinity` and `NaN` (via YAML `.inf`, `.nan`). Config validation would succeed, but the agents SDK guards every trigger path with `Number.isFinite(...)` and silently returns `false` — summarization never fires while the server starts cleanly. That is the exact schema/runtime split this PR is trying to eliminate. Add `.finite()` to every trigger value. `token_ratio` already had an implicit guard via `.max(1)`, but applying `.finite()` uniformly keeps the intent obvious and catches `NaN` (which `.max(1)` does not). --- packages/data-provider/specs/config-schemas.spec.ts | 8 ++++++++ packages/data-provider/src/config.ts | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/data-provider/specs/config-schemas.spec.ts b/packages/data-provider/specs/config-schemas.spec.ts index 7c45ce812f14..5e82803d7618 100644 --- a/packages/data-provider/specs/config-schemas.spec.ts +++ b/packages/data-provider/specs/config-schemas.spec.ts @@ -579,6 +579,14 @@ describe('summarizationTriggerSchema', () => { ).toBe(true); }); + it('rejects non-finite values (Infinity, NaN) for every trigger type', () => { + for (const type of ['token_ratio', 'remaining_tokens', 'messages_to_refine'] as const) { + expect(summarizationTriggerSchema.safeParse({ type, value: Infinity }).success).toBe(false); + expect(summarizationTriggerSchema.safeParse({ type, value: -Infinity }).success).toBe(false); + expect(summarizationTriggerSchema.safeParse({ type, value: NaN }).success).toBe(false); + } + }); + it('parses inside the full summarization config', () => { const result = summarizationConfigSchema.safeParse({ enabled: true, diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 6db847e9763a..eee5dc7bf1c7 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1023,15 +1023,15 @@ export type TMemoryConfig = DeepPartial>; export const summarizationTriggerSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('token_ratio'), - value: z.number().min(0).max(1), + value: z.number().finite().min(0).max(1), }), z.object({ type: z.literal('remaining_tokens'), - value: z.number().positive(), + value: z.number().finite().positive(), }), z.object({ type: z.literal('messages_to_refine'), - value: z.number().positive(), + value: z.number().finite().positive(), }), ]); From 7521b7e8092b3bcdcb0baccda5d234ee07ca4cb9 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 18 Apr 2026 11:30:59 -0400 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=A7=B9=20fix:=20integer=20counts=20+?= =?UTF-8?q?=20targeted=20token=5Fcount=20migration=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings from the comprehensive review: 1. `remaining_tokens` and `messages_to_refine` are token/message counts and are always integers in the runtime (`Number.isFinite(...)` guards already assume integer semantics). `z.number().positive()` accepted fractional values like `2.5`, which was semantically confusing and would round oddly against the runtime's `>=` / `<=` comparisons. Add `.int()` to both count-based branches; `token_ratio` stays fractional. 2. Anyone upgrading with `trigger.type: 'token_count'` in their YAML got the generic "Invalid summarization config" warning plus a flattened Zod error. Detect that specific case in `loadSummarizationConfig` and emit a migration-friendly message that names the three valid replacements. Export the function so the behavior is unit-testable. Also added a parameterized passthrough test covering `remaining_tokens` and `messages_to_refine` shaping, complementing the existing `token_ratio` coverage. --- .../__tests__/run-summarization.test.ts | 14 ++++ .../specs/config-schemas.spec.ts | 15 +++++ packages/data-provider/src/config.ts | 4 +- packages/data-schemas/src/app/service.spec.ts | 67 +++++++++++++++++++ packages/data-schemas/src/app/service.ts | 14 +++- 5 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 packages/data-schemas/src/app/service.spec.ts diff --git a/packages/api/src/agents/__tests__/run-summarization.test.ts b/packages/api/src/agents/__tests__/run-summarization.test.ts index 5699d2ad45a2..8c4f94d8ad93 100644 --- a/packages/api/src/agents/__tests__/run-summarization.test.ts +++ b/packages/api/src/agents/__tests__/run-summarization.test.ts @@ -265,6 +265,20 @@ describe('summarizationConfig field passthrough', () => { const config = agents[0].summarizationConfig as Record; expect(config.trigger).toEqual({ type: 'token_ratio', value: 0 }); }); + + it.each([ + ['remaining_tokens', 500], + ['messages_to_refine', 4], + ] as const)('passes %s trigger through unchanged', async (type, value) => { + const agents = await callAndCapture({ + summarizationConfig: { + enabled: true, + trigger: { type, value }, + }, + }); + const config = agents[0].summarizationConfig as Record; + expect(config.trigger).toEqual({ type, value }); + }); }); // --------------------------------------------------------------------------- diff --git a/packages/data-provider/specs/config-schemas.spec.ts b/packages/data-provider/specs/config-schemas.spec.ts index 5e82803d7618..c5bb6639cd68 100644 --- a/packages/data-provider/specs/config-schemas.spec.ts +++ b/packages/data-provider/specs/config-schemas.spec.ts @@ -587,6 +587,21 @@ describe('summarizationTriggerSchema', () => { } }); + it('requires integer values for count-based triggers', () => { + expect( + summarizationTriggerSchema.safeParse({ type: 'remaining_tokens', value: 500.5 }).success, + ).toBe(false); + expect( + summarizationTriggerSchema.safeParse({ type: 'messages_to_refine', value: 2.5 }).success, + ).toBe(false); + }); + + it('still allows fractional values for token_ratio', () => { + expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: 0.8 }).success).toBe( + true, + ); + }); + it('parses inside the full summarization config', () => { const result = summarizationConfigSchema.safeParse({ enabled: true, diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index eee5dc7bf1c7..7203c8d89bbb 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1027,11 +1027,11 @@ export const summarizationTriggerSchema = z.discriminatedUnion('type', [ }), z.object({ type: z.literal('remaining_tokens'), - value: z.number().finite().positive(), + value: z.number().finite().int().positive(), }), z.object({ type: z.literal('messages_to_refine'), - value: z.number().finite().positive(), + value: z.number().finite().int().positive(), }), ]); diff --git a/packages/data-schemas/src/app/service.spec.ts b/packages/data-schemas/src/app/service.spec.ts new file mode 100644 index 000000000000..324f6e466beb --- /dev/null +++ b/packages/data-schemas/src/app/service.spec.ts @@ -0,0 +1,67 @@ +import type { DeepPartial, TCustomConfig } from 'librechat-data-provider'; +import { loadSummarizationConfig } from './service'; +import logger from '~/config/winston'; + +jest.mock('~/config/winston', () => ({ + __esModule: true, + default: { + warn: jest.fn(), + info: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, +})); + +describe('loadSummarizationConfig', () => { + const warnSpy = logger.warn as jest.Mock; + + beforeEach(() => { + warnSpy.mockClear(); + }); + + it('returns undefined when no summarization config is provided', () => { + expect(loadSummarizationConfig({} as DeepPartial)).toBeUndefined(); + }); + + it('accepts a valid token_ratio trigger', () => { + const result = loadSummarizationConfig({ + summarization: { + enabled: true, + trigger: { type: 'token_ratio', value: 0.8 }, + }, + } as DeepPartial); + + expect(result).toBeDefined(); + expect(result?.enabled).toBe(true); + expect(result?.trigger).toEqual({ type: 'token_ratio', value: 0.8 }); + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('emits a targeted migration warning when trigger.type is the legacy "token_count"', () => { + const result = loadSummarizationConfig({ + summarization: { + trigger: { type: 'token_count', value: 8000 }, + }, + } as unknown as DeepPartial); + + expect(result).toBeUndefined(); + expect(warnSpy).toHaveBeenCalledTimes(1); + const message = String(warnSpy.mock.calls[0][0]); + expect(message).toContain('token_count'); + expect(message).toContain('token_ratio'); + expect(message).toContain('remaining_tokens'); + expect(message).toContain('messages_to_refine'); + }); + + it('falls back to the generic warning for other schema violations', () => { + const result = loadSummarizationConfig({ + summarization: { + trigger: { type: 'token_ratio', value: 80 }, + }, + } as unknown as DeepPartial); + + expect(result).toBeUndefined(); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(String(warnSpy.mock.calls[0][0])).toContain('Invalid summarization config'); + }); +}); diff --git a/packages/data-schemas/src/app/service.ts b/packages/data-schemas/src/app/service.ts index 91407b06c40d..8cf84e84c47f 100644 --- a/packages/data-schemas/src/app/service.ts +++ b/packages/data-schemas/src/app/service.ts @@ -15,12 +15,24 @@ import { loadEndpoints } from './endpoints'; import { loadOCRConfig } from './ocr'; import logger from '~/config/winston'; -function loadSummarizationConfig(config: DeepPartial): AppConfig['summarization'] { +export function loadSummarizationConfig( + config: DeepPartial, +): AppConfig['summarization'] { const raw = config.summarization; if (!raw || typeof raw !== 'object') { return undefined; } + if (raw.trigger && typeof raw.trigger === 'object' && raw.trigger.type === 'token_count') { + logger.warn( + "[AppService] `summarization.trigger.type: 'token_count'` is no longer supported. " + + "Use 'token_ratio' (0–1), 'remaining_tokens' (positive integer), or " + + "'messages_to_refine' (positive integer). Summarization will be disabled until " + + 'this is fixed.', + ); + return undefined; + } + const parsed = summarizationConfigSchema.safeParse(raw); if (!parsed.success) { logger.warn('[AppService] Invalid summarization config', parsed.error.flatten()); From 3d11e2602e7ae72f7bb60ffb12601a7be278c2b4 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 18 Apr 2026 12:09:37 -0400 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=A7=B9=20fix:=20accurate=20fallback?= =?UTF-8?q?=20wording=20+=20bare-string=20trigger=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two nits from the follow-up audit: 1. The legacy-`token_count` warning claimed "Summarization will be disabled," but `shapeSummarizationConfig` treats a missing summarization config as self-summarize mode (fires on every pruning event using the agent's own provider/model). "Disabled" would mislead an admin into stopping investigation. Reword to describe the actual fallback and assert the new wording in the spec. 2. Add a regression test for the `trigger: 'bare-string'` YAML case, so the `typeof raw.trigger === 'object'` guard is exercised rather than implied. 3. Swap the en-dash in `(0–1)` for an ASCII hyphen so the log message is safe in every terminal/aggregator regardless of UTF-8 handling. --- packages/data-schemas/src/app/service.spec.ts | 13 +++++++++++++ packages/data-schemas/src/app/service.ts | 8 +++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/data-schemas/src/app/service.spec.ts b/packages/data-schemas/src/app/service.spec.ts index 324f6e466beb..80298b3e1821 100644 --- a/packages/data-schemas/src/app/service.spec.ts +++ b/packages/data-schemas/src/app/service.spec.ts @@ -51,6 +51,19 @@ describe('loadSummarizationConfig', () => { expect(message).toContain('token_ratio'); expect(message).toContain('remaining_tokens'); expect(message).toContain('messages_to_refine'); + expect(message).toContain('fall back'); + }); + + it('falls back to the generic warning when trigger is a bare string (not an object)', () => { + const result = loadSummarizationConfig({ + summarization: { + trigger: 'token_count', + }, + } as unknown as DeepPartial); + + expect(result).toBeUndefined(); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(String(warnSpy.mock.calls[0][0])).toContain('Invalid summarization config'); }); it('falls back to the generic warning for other schema violations', () => { diff --git a/packages/data-schemas/src/app/service.ts b/packages/data-schemas/src/app/service.ts index 8cf84e84c47f..aefea713fb5c 100644 --- a/packages/data-schemas/src/app/service.ts +++ b/packages/data-schemas/src/app/service.ts @@ -26,9 +26,11 @@ export function loadSummarizationConfig( if (raw.trigger && typeof raw.trigger === 'object' && raw.trigger.type === 'token_count') { logger.warn( "[AppService] `summarization.trigger.type: 'token_count'` is no longer supported. " + - "Use 'token_ratio' (0–1), 'remaining_tokens' (positive integer), or " + - "'messages_to_refine' (positive integer). Summarization will be disabled until " + - 'this is fixed.', + "Use 'token_ratio' (0-1), 'remaining_tokens' (positive integer), or " + + "'messages_to_refine' (positive integer). Your `summarization` config will be " + + 'ignored and summarization will fall back to self-summarize defaults (the ' + + "agent's own provider/model, fires on every pruning event) until this is " + + 'corrected.', ); return undefined; } From 617e1660735af105895e356833484db60978f3c7 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 18 Apr 2026 12:15:01 -0400 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=94=87=20fix:=20cast=20`raw.trigger.t?= =?UTF-8?q?ype`=20to=20inspect=20legacy=20value=20past=20narrowed=20union?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI TS check failed: after the schema tightening, `raw.trigger.type` is narrowed to `"token_ratio" | "remaining_tokens" | "messages_to_refine" | undefined`, so the runtime comparison to `"token_count"` is a TS2367 ("no overlap") error even though that's exactly the comparison we want for the migration guard. Widen just that one access via `as { type?: unknown }` so the migration check reads runtime-shaped YAML input without the type system folding it back into the narrowed union. --- packages/data-schemas/src/app/service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/data-schemas/src/app/service.ts b/packages/data-schemas/src/app/service.ts index aefea713fb5c..57a5e603ac34 100644 --- a/packages/data-schemas/src/app/service.ts +++ b/packages/data-schemas/src/app/service.ts @@ -23,7 +23,11 @@ export function loadSummarizationConfig( return undefined; } - if (raw.trigger && typeof raw.trigger === 'object' && raw.trigger.type === 'token_count') { + if ( + raw.trigger && + typeof raw.trigger === 'object' && + (raw.trigger as { type?: unknown }).type === 'token_count' + ) { logger.warn( "[AppService] `summarization.trigger.type: 'token_count'` is no longer supported. " + "Use 'token_ratio' (0-1), 'remaining_tokens' (positive integer), or " +