fix(core): ensure hard threshold always exceeds auto threshold (#4945) (#4949)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run

When the proportional branch dominated auto (e.g. 60K context window), rawHard fell below auto, causing hard to degrade to auto. This meant compaction only triggered at the very last moment with no gap between auto and hard tiers.

Change hard from max(rawHard, auto) to min(window, max(rawHard, auto + HARD_BUFFER)), guaranteeing hard > auto for all non-zero windows while preserving behavior for large windows where rawHard already dominates.

Closes #4945
This commit is contained in:
DennisYu07 2026-06-10 17:24:10 +08:00 committed by GitHub
parent 203f8ef46d
commit 01bd2b64d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 21 additions and 7 deletions

View file

@ -1883,14 +1883,23 @@ describe('ChatCompressionService.compress cheap-gate uses estimated tokens', ()
});
describe('computeThresholds', () => {
it('32K window — proportional fallback for all tiers, hard degrades to auto', () => {
it('32K window — proportional fallback for all tiers, hard = auto + HARD_BUFFER', () => {
const t = computeThresholds(32_000);
expect(t.warn).toBe(19_200); // 0.6 * 32K
expect(t.auto).toBe(22_400); // 0.7 * 32K
expect(t.hard).toBe(22_400); // max(window-23K=9K, auto=22.4K) = auto
expect(t.hard).toBe(25_400); // auto + HARD_BUFFER = 22.4K + 3K
expect(t.effectiveWindow).toBe(12_000);
});
it('60K window — hard no longer equals auto (issue #4945)', () => {
const t = computeThresholds(60_000);
expect(t.warn).toBe(36_000); // 0.6 * 60K
expect(t.auto).toBe(42_000); // 0.7 * 60K (pct wins: 42K vs ew-13K=27K)
expect(t.hard).toBe(45_000); // auto + HARD_BUFFER = 42K + 3K
expect(t.hard).toBeGreaterThan(t.auto);
expect(t.effectiveWindow).toBe(40_000);
});
it('128K window — mixed (warn=pct, auto/hard=abs)', () => {
const t = computeThresholds(128_000);
expect(t.warn).toBe(76_800); // 0.6 * 128K (pct wins: 76.8K vs auto-20K=75K)
@ -1933,11 +1942,13 @@ describe('computeThresholds', () => {
expect(t.hard).toBe(0);
});
it('thresholds always satisfy warn <= auto <= hard', () => {
for (const w of [32_000, 64_000, 128_000, 200_000, 256_000, 1_000_000]) {
it('thresholds always satisfy warn <= auto < hard for non-zero windows', () => {
for (const w of [
10_000, 32_000, 60_000, 64_000, 128_000, 200_000, 256_000, 1_000_000,
]) {
const t = computeThresholds(w);
expect(t.warn).toBeLessThanOrEqual(t.auto);
expect(t.auto).toBeLessThanOrEqual(t.hard);
expect(t.auto).toBeLessThan(t.hard);
}
});
});

View file

@ -120,7 +120,7 @@ export interface CompactionThresholds {
* Each tier is `max(proportional, absolute)`:
* auto = max(DEFAULT_PCT * window, effectiveWindow - AUTOCOMPACT_BUFFER)
* warn = max((DEFAULT_PCT - WARN_PCT_OFFSET) * window, auto - WARN_BUFFER)
* hard = max(effectiveWindow - HARD_BUFFER, auto) // hard degrades to auto for tiny windows
* hard = min(window, max(effectiveWindow - HARD_BUFFER, auto + HARD_BUFFER))
*
* Small windows (where the absolute branch goes negative) automatically
* fall back to the proportional branch. Large windows are dominated by
@ -144,7 +144,10 @@ export function computeThresholds(window: number): CompactionThresholds {
const warn = Math.max((DEFAULT_PCT - WARN_PCT_OFFSET) * window, absWarn);
const rawHard = effectiveWindow - HARD_BUFFER;
const hard = Math.max(rawHard, auto);
// Guarantee hard > auto so compaction doesn't wait until the last moment.
// For tiny/zero windows where auto is already at the proportional floor,
// clamp hard to the window itself so it never exceeds the actual limit.
const hard = Math.min(window, Math.max(rawHard, auto + HARD_BUFFER));
return { warn, auto, hard, effectiveWindow };
}