From 8f840a6b7fe489b63353e36f9d875d0b874d2059 Mon Sep 17 00:00:00 2001 From: tt-a1i <53142663+tt-a1i@users.noreply.github.com> Date: Mon, 22 Jun 2026 20:59:45 +0800 Subject: [PATCH] fix(core): require integer compaction counts --- .../services/compactionInputSlimming.test.ts | 63 +++++++++++++++++++ .../src/services/compactionInputSlimming.ts | 37 +++++++---- 2 files changed, 87 insertions(+), 13 deletions(-) diff --git a/packages/core/src/services/compactionInputSlimming.test.ts b/packages/core/src/services/compactionInputSlimming.test.ts index 678e47aa650..a91c5741430 100644 --- a/packages/core/src/services/compactionInputSlimming.test.ts +++ b/packages/core/src/services/compactionInputSlimming.test.ts @@ -123,6 +123,69 @@ describe('compactionInputSlimming', () => { expect(t.screenshotTriggerThreshold).toBe(99); }); + it('rejects fractional count-like env values', () => { + const cases = [ + { + envKey: 'QWEN_COMPACT_MAX_RECENT_FILES', + value: '1.5', + settings: { maxRecentFilesToRetain: 4 }, + get: (t: ReturnType) => + t.maxRecentFiles, + expected: 4, + }, + { + envKey: 'QWEN_COMPACT_MAX_RECENT_IMAGES', + value: '2.5', + settings: { maxRecentImagesToRetain: 5 }, + get: (t: ReturnType) => + t.maxRecentImages, + expected: 5, + }, + { + envKey: 'QWEN_COMPACT_SCREENSHOT_THRESHOLD', + value: '9007199254740990.5', + settings: { screenshotTriggerThreshold: 6 }, + get: (t: ReturnType) => + t.screenshotTriggerThreshold, + expected: 6, + }, + ] as const; + + for (const c of cases) { + for (const k of COMPACTION_ENV_KEYS) delete process.env[k]; + process.env[c.envKey] = c.value; + expect(c.get(resolveCompactionTuning(c.settings))).toBe(c.expected); + } + }); + + it('rejects fractional count-like settings values', () => { + const t = resolveCompactionTuning({ + maxRecentFilesToRetain: 1.5, + maxRecentImagesToRetain: 2.5, + screenshotTriggerThreshold: 3.5, + }); + expect(t.maxRecentFiles).toBe(DEFAULT_MAX_RECENT_FILES); + expect(t.maxRecentImages).toBe(DEFAULT_MAX_RECENT_IMAGES); + expect(t.screenshotTriggerThreshold).toBe( + DEFAULT_SCREENSHOT_TRIGGER_THRESHOLD, + ); + }); + + it('rejects unsafe integer count-like values', () => { + process.env['QWEN_COMPACT_MAX_RECENT_FILES'] = String( + Number.MAX_SAFE_INTEGER + 1, + ); + const envFallback = resolveCompactionTuning({ + maxRecentFilesToRetain: 4, + }); + expect(envFallback.maxRecentFiles).toBe(4); + + const settingsFallback = resolveCompactionTuning({ + maxRecentImagesToRetain: Number.MAX_SAFE_INTEGER + 1, + }); + expect(settingsFallback.maxRecentImages).toBe(DEFAULT_MAX_RECENT_IMAGES); + }); + it('parses the boolean env both ways and ignores typos', () => { process.env['QWEN_COMPACT_SCREENSHOT_TRIGGER'] = 'false'; expect(resolveCompactionTuning(undefined).enableScreenshotTrigger).toBe( diff --git a/packages/core/src/services/compactionInputSlimming.ts b/packages/core/src/services/compactionInputSlimming.ts index 17ae2284cca..52b44098e9d 100644 --- a/packages/core/src/services/compactionInputSlimming.ts +++ b/packages/core/src/services/compactionInputSlimming.ts @@ -80,19 +80,29 @@ function resolveNumber( envValue: string | undefined, settingsValue: number | undefined, defaultValue: number, - { minInclusive }: { minInclusive: number }, + { + integer = false, + minInclusive, + }: { integer?: boolean; minInclusive: number }, ): number { + const isValid = (value: number) => + Number.isFinite(value) && + (!integer || Number.isSafeInteger(value)) && + value >= minInclusive; + if (envValue !== undefined && envValue !== '') { - const parsed = Number(envValue); - if (Number.isFinite(parsed) && parsed >= minInclusive) { + const trimmed = envValue.trim(); + if (integer && !/^\d+$/.test(trimmed)) { + return settingsValue !== undefined && isValid(settingsValue) + ? settingsValue + : defaultValue; + } + const parsed = Number(trimmed); + if (isValid(parsed)) { return parsed; } } - if ( - settingsValue !== undefined && - Number.isFinite(settingsValue) && - settingsValue >= minInclusive - ) { + if (settingsValue !== undefined && isValid(settingsValue)) { return settingsValue; } return defaultValue; @@ -116,8 +126,9 @@ export interface ResolvedCompactionTuning { /** * Resolves the post-compact retention + screenshot-trigger knobs in - * priority order env > settings > default, reusing the same validation - * rules as `resolveSlimmingConfig`. + * priority order env > settings > default. Count-like fields require + * integer values because downstream collectors compare against integer + * lengths. * * The screenshot trigger counts only images nested in * `functionResponse.parts` (tool results). Compaction replaces those with @@ -134,13 +145,13 @@ export function resolveCompactionTuning( process.env['QWEN_COMPACT_MAX_RECENT_FILES'], settings?.maxRecentFilesToRetain, DEFAULT_MAX_RECENT_FILES, - { minInclusive: 0 }, + { integer: true, minInclusive: 0 }, ), maxRecentImages: resolveNumber( process.env['QWEN_COMPACT_MAX_RECENT_IMAGES'], settings?.maxRecentImagesToRetain, DEFAULT_MAX_RECENT_IMAGES, - { minInclusive: 0 }, + { integer: true, minInclusive: 0 }, ), enableScreenshotTrigger: resolveBoolean( process.env['QWEN_COMPACT_SCREENSHOT_TRIGGER'], @@ -151,7 +162,7 @@ export function resolveCompactionTuning( process.env['QWEN_COMPACT_SCREENSHOT_THRESHOLD'], settings?.screenshotTriggerThreshold, DEFAULT_SCREENSHOT_TRIGGER_THRESHOLD, - { minInclusive: 1 }, + { integer: true, minInclusive: 1 }, ), }; }