diff --git a/devlog/2026-07-30_filter-recommended-ranges-fix/REQ.md b/devlog/2026-07-30_filter-recommended-ranges-fix/REQ.md new file mode 100644 index 0000000..bf22647 --- /dev/null +++ b/devlog/2026-07-30_filter-recommended-ranges-fix/REQ.md @@ -0,0 +1,35 @@ +# REQ: Fix filterRecommendedRanges suppressing ranges at large context windows (Issue #251) + +## Problem + +`filterRecommendedRanges` used a context-relative threshold (`modelContextLimit × 5%`) to suppress compression recommendations when the "effective compressible" tokens were below the threshold. At 1M context windows (Claude Sonnet 4.5), this threshold was 50K — meaning sessions with 30-40K of genuinely compressible content never received compression recommendations, even when the nudge system correctly detected growth. + +## Impact + +- Sessions at 1M context never get compression recommendations until 50K+ tokens accumulate +- Nudge fires (growth ≥ threshold) but immediately suppressed → `nothingToCompress = true` → no nudge text injected +- Model never sees compressible ranges → context grows unbounded → eventually hits hard limit + +## Root Cause + +`filterRecommendedRanges` in `lib/messages/inject/utils.ts`: +- `growthThreshold = modelContextLimit × 0.05` (50K at 1M, 10K at 200K) +- `lastSegmentFloor = growthThreshold × 2` (100K at 1M, 20K at 200K) +- Aggregate `effectiveCompressible` (non-last ranges + excess of last range beyond floor) compared against `growthThreshold` +- If below threshold → return `[]` → `filterSuppressed = true` → `nothingToCompress = true` + +## Fix + +**Simplify `filterRecommendedRanges`**: remove all suppression logic. Always return all input ranges, marking the last segment as `dangerous: true`. + +**Backstop**: `minCompressRange` in `range.ts:185` (5000 chars ≈ 1250 tokens) already prevents garbage compressions at the compress tool level. + +**Clean up dead code**: Remove `filterSuppressed` variable from `nothingToCompress` calculation (always `false` after fix). + +## Files Changed + +- `lib/messages/inject/utils.ts` — rewrite `filterRecommendedRanges`, simplify `RangeFilterOptions` +- `lib/messages/inject/inject.ts` — update call site, remove `filterSuppressed`, update debug logging +- `tests/smart-nudge-gating.test.ts` — rewrite for new behavior (8 tests) +- `tests/property-invariants.test.ts` — rewrite INV5 (always returns all ranges) +- `tests/inject.test.ts` — update 2 tests for new behavior diff --git a/devlog/2026-07-30_filter-recommended-ranges-fix/WORKLOG.md b/devlog/2026-07-30_filter-recommended-ranges-fix/WORKLOG.md new file mode 100644 index 0000000..f625699 --- /dev/null +++ b/devlog/2026-07-30_filter-recommended-ranges-fix/WORKLOG.md @@ -0,0 +1,38 @@ +# WORKLOG: Fix filterRecommendedRanges suppressing ranges at large context windows + +## Changes + +### `lib/messages/inject/utils.ts` +- Rewrote `filterRecommendedRanges`: removed `growthThreshold`, `lastSegmentFloor`, `suppressed`, `effectiveCompressible` logic +- New behavior: always return all input ranges, last segment marked `dangerous: true` +- Simplified `RangeFilterOptions`: removed `modelContextLimit` and `growthRatio`, only `logger` remains + +### `lib/messages/inject/inject.ts` +- Updated `filterRecommendedRanges` call: `{ logger }` instead of `{ modelContextLimit, growthRatio: 0.05, logger }` +- Removed `filterSuppressed` from `nothingToCompress` calculation (always false after fix) +- Updated debug logging: removed `growthThreshold`, `lastSegmentFloor`, `suppressed` references + +### Tests +- `tests/smart-nudge-gating.test.ts`: Rewritten — 8 tests covering new behavior (no suppression, last gets dangerous, tiny ranges shown, etc.) +- `tests/property-invariants.test.ts`: INV5 rewritten — property: always returns all input ranges (300 random runs) +- `tests/inject.test.ts`: 2 tests updated — old "nudge suppressed" tests now assert nudge fires + +## Verification + +- `npm run typecheck` — clean +- `npm test` — 942 tests pass, 0 failures + +## Additional Fix: Nudge Loop Bug (lastNudgeShownTokens reset) + +### Problem +When `nothingToCompress` was true (all ranges protected), `lastNudgeShownTokens` was reset to `undefined`. This caused a loop: next turn, `growthReference` fell back to stale `lastPerMessageNudgeTokens` (potentially very old), producing artificially huge growth → nudge fires → nothingToCompress again → reset → repeat every turn. + +### Changes +- `lib/messages/inject/inject.ts`: + - Removed `lastNudgeShownTokens = undefined` from `nothingToCompress` path (lines 367-369 deleted). Baseline preserved, half-threshold gate applies naturally. + - Fixed growth display: `currentTokens - lastPerMessageNudgeTokens` → `currentTokens - (lastNudgeShownTokens ?? lastPerMessageNudgeTokens)`. Display now matches the actual growthReference used for nudge decisions. + +### Tests +- `tests/inject.test.ts`: + - Updated "pending nudge cleared when suppressed" → "pending nudge preserved when all-protected — no loop" (asserts baseline kept, not reset) + - Added "multi-turn: all-protected does not loop (lastNudgeShownTokens stable)" — 3-turn regression test verifying baseline stability diff --git a/lib/messages/inject/inject.ts b/lib/messages/inject/inject.ts index 49564ba..06d4f51 100644 --- a/lib/messages/inject/inject.ts +++ b/lib/messages/inject/inject.ts @@ -343,42 +343,30 @@ export const injectCompressNudges = ( const recommendedRanges = filterRecommendedRanges( unprotectedCompressible, contextRanges.protected, - { modelContextLimit, growthRatio: 0.05, logger }, + { logger }, ) const hasRecommendations = recommendedRanges.length > 0 - if (config.debug && contextRanges.compressible.length > 0 && modelContextLimit) { - const growthThreshold = modelContextLimit * 0.05 - const lastSegmentFloor = growthThreshold * 2 + if (config.debug && contextRanges.compressible.length > 0) { const compressible = contextRanges.compressible - const lastRange = compressible[compressible.length - 1] - const lastIncluded = lastRange.tokens >= lastSegmentFloor - const nonLastTokens = compressible - .slice(0, -1) - .reduce((s, r) => s + r.tokens, 0) - const effective = nonLastTokens + Math.max(0, lastRange.tokens - lastSegmentFloor) - const suppressed = effective < growthThreshold const fmt = (n: number) => (n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n)) const lines = [ - `[ACP Debug] Recommendation filter decision:`, + `[ACP Debug] Recommendation filter:`, ` Input: ${compressible.length} range(s), ${fmt(compressible.reduce((s, r) => s + r.tokens, 0))} tokens`, - ` Thresholds: growth=${fmt(growthThreshold)}, lastSegmentFloor=${fmt(lastSegmentFloor)}`, - ` Last segment: ${lastRange.startRef}–${lastRange.endRef} ${fmt(lastRange.tokens)} tokens → ${lastIncluded ? "included (dangerous)" : "excluded (< floor)"}`, - ` Effective compressible: ${fmt(effective)} → ${suppressed ? "SUPPRESSED (< growth threshold)" : `${recommendedRanges.length} range(s) recommended`}`, + ` Output: ${recommendedRanges.length} range(s) (last segment marked dangerous)`, ] logger.debug(lines.join("\n")) } - const filterSuppressed = contextRanges.compressible.length > 0 && !hasRecommendations const allProtected = contextRanges.compressible.length === 0 && contextRanges.protected.length > 0 const allInProtectedZone = protectedRefs.size > 0 && unprotectedCompressible.length === 0 - const nothingToCompress = filterSuppressed || allProtected || allInProtectedZone + const nothingToCompress = allProtected || allInProtectedZone const shouldInjectNudge = nudgeAllowed && (!nothingToCompress || emergencyOverride) let shouldInject = shouldInjectNudge - if (nudgeAllowed && nothingToCompress && !emergencyOverride) { - state.nudges.lastNudgeShownTokens = undefined - } + // Keep lastNudgeShownTokens when nothingToCompress — resetting it + // reintroduces the nudge loop (baseline wiped → stale growthReference + // → nudge fires every turn). // Issue #216 Defect 1: only apply anchored nudge text when there IS something // to compress. Previously applyAnchoredNudges ran before nothingToCompress was @@ -506,8 +494,9 @@ export const injectCompressNudges = ( const pct = (n: number) => n > 0 ? Math.max(1, Math.round((n / composition.total) * 100)) : 0 const growth = - currentTokens !== undefined && state.nudges.lastPerMessageNudgeTokens !== undefined - ? currentTokens - state.nudges.lastPerMessageNudgeTokens + currentTokens !== undefined && + (state.nudges.lastNudgeShownTokens ?? state.nudges.lastPerMessageNudgeTokens) !== undefined + ? currentTokens - (state.nudges.lastNudgeShownTokens ?? state.nudges.lastPerMessageNudgeTokens!) : 0 const growthStr = growth > 0 ? ` (+${fmt(growth)} since last nudge)` : "" diff --git a/lib/messages/inject/utils.ts b/lib/messages/inject/utils.ts index c65cdfe..12c9842 100644 --- a/lib/messages/inject/utils.ts +++ b/lib/messages/inject/utils.ts @@ -761,92 +761,46 @@ export function buildCompressibleRanges( } export interface RangeFilterOptions { - modelContextLimit: number | undefined - growthRatio: number logger?: { debug: (msg: string, data?: any) => void } } /** * Filter compressible ranges for the recommendation list. * - * Last segment: excluded if < 2× growth threshold; included with - * `dangerous: true` flag if >= 2× growth threshold. + * All ranges are shown to the model — the model decides what to compress. + * The last segment is always marked `dangerous: true` (it may still be in + * active use; the model is warned in the suffix text). * - * Gate: "effective compressible" = non-last-segment tokens + max(0, - * last-segment tokens − 2× growth threshold). If effective compressible - * < growth threshold, suppress all recommendations. + * Issue #251: Previously this function used `growthThreshold` (5% of context + * window = 50K at 1M) as an aggregate gate — if "effective compressible" + * was below the threshold, ALL ranges were suppressed and the nudge was + * hidden. At large context windows, individual ranges rarely exceeded this + * threshold, so compression was permanently blocked. The aggregate gate + * has been removed; `minCompressRange` in `range.ts` (5000 chars) already + * prevents garbage compressions as a backstop. */ export function filterRecommendedRanges( compressible: CompressibleRange[], _protectedRanges: ProtectedRange[], options: RangeFilterOptions, ): CompressibleRange[] { - const { modelContextLimit, growthRatio, logger } = options + const { logger } = options const log = logger?.debug.bind(logger) - if (!modelContextLimit || modelContextLimit <= 0) { - log?.("filterRecommendedRanges: modelContextLimit unknown, passthrough", { - inputCount: compressible.length, - }) - if (compressible.length === 0) return [] - const passthrough = [...compressible] - passthrough[passthrough.length - 1] = { - ...passthrough[passthrough.length - 1], - dangerous: true, - } - return passthrough - } - if (compressible.length === 0) { log?.("filterRecommendedRanges: no compressible ranges, returning empty") return [] } - const growthThreshold = modelContextLimit * growthRatio - const lastSegmentFloor = growthThreshold * 2 - - const lastIndex = compressible.length - 1 - const lastRange = compressible[lastIndex] - const lastSegmentIncluded = lastRange.tokens >= lastSegmentFloor - - let effectiveCompressible = 0 - const result: CompressibleRange[] = [] - - for (let i = 0; i < compressible.length; i++) { - const r = compressible[i] - if (i === lastIndex) { - const excess = Math.max(0, r.tokens - lastSegmentFloor) - effectiveCompressible += excess - if (lastSegmentIncluded) { - result.push({ ...r, dangerous: true }) - } - } else { - effectiveCompressible += r.tokens - result.push(r) - } - } - - const suppressed = effectiveCompressible < growthThreshold + const result = compressible.map((r, i) => + i === compressible.length - 1 ? { ...r, dangerous: true } : r, + ) - log?.("filterRecommendedRanges: decision", { + log?.("filterRecommendedRanges: passthrough (last segment marked dangerous)", { inputRanges: compressible.length, - inputTokens: compressible.reduce((s, r) => s + r.tokens, 0), - growthThreshold, - lastSegmentFloor, - lastSegment: { - ref: `${lastRange.startRef}–${lastRange.endRef}`, - tokens: lastRange.tokens, - included: lastSegmentIncluded, - }, - effectiveCompressible, - suppressed, - outputRanges: suppressed ? 0 : result.length, + outputRanges: result.length, }) - if (suppressed) { - return [] - } - return result } diff --git a/tests/inject.test.ts b/tests/inject.test.ts index 3464514..8d0c7d7 100644 --- a/tests/inject.test.ts +++ b/tests/inject.test.ts @@ -751,11 +751,11 @@ test("growth floor: 98% emergency override fires regardless of growth", () => { ) }) -test("nudge suppressed when filter has no recommendations (all ranges below last-segment floor)", () => { - // 1M model: growthThreshold=50K, lastSegmentFloor=100K +test("nudge fires when small ranges exist — Issue #251: no floor suppression at large context", () => { + // 1M model: growthThreshold=50K // Growth of 55K > 50K threshold → nudgeAllowed = true - // But tool output is 80K chars (~20K tokens) < 100K floor → filtered out - // shouldInjectThisTurn = false (no ranges to recommend, not emergency) + // Tool output is 80K chars (~20K tokens) — before #251 this was < 100K floor → suppressed + // After #251: filterRecommendedRanges never suppresses → range shown → nudge fires const state = createSessionState() state.modelContextLimit = 1_000_000 state.nudges.lastPerMessageNudgeTokens = 200_000 @@ -776,14 +776,13 @@ test("nudge suppressed when filter has no recommendations (all ranges below last assert.equal( state.nudges.shouldInjectThisTurn, - false, - "55K growth triggers nudgeAllowed but 20K tool output < 100K floor → no recommendations → nudge suppressed", + true, + "55K growth + 20K tool output → range recommended → nudge fires (Issue #251 fix)", ) const injected = suffixText(messages) - assert.ok(!injected.includes("Breakdown:"), "no breakdown when no recommendations") - assert.ok(!injected.includes("efficiency nudge"), "no efficiency nudge text") - assert.ok(!injected.includes("Context limit reached"), "no emergency alert") + assert.ok(injected.includes("Breakdown:"), "breakdown shown when range recommended") + assert.ok(!injected.includes("Context limit reached"), "no emergency alert — not at max limit") }) test("nudge suppressed when all content is protected (nothing to compress)", () => { @@ -901,7 +900,7 @@ test("baseline preserved when nudge suppressed — growth accumulates (all prote ) }) -test("baseline preserved when filter suppressed — compressible too small", () => { +test("baseline preserved when nudge fires for small compressible — Issue #251", () => { const state = createSessionState() state.modelContextLimit = 1_000_000 state.messageIds.byRawId.set("u1", "m00001") @@ -919,15 +918,15 @@ test("baseline preserved when filter suppressed — compressible too small", () ]), ] injectCompressNudges(state, config, logger, turn1, {} as any) - assert.equal(state.nudges.shouldInjectThisTurn, false, "55K growth but 20K compressible < 100K floor → suppressed") + assert.equal(state.nudges.shouldInjectThisTurn, true, "55K growth + 20K compressible → nudge fires (Issue #251)") assert.equal( state.nudges.lastPerMessageNudgeTokens, 200_000, - "baseline preserved — growth accumulates until compressible content exists", + "baseline preserved on nudge fire — only advances after actual compression (inject.ts:537)", ) }) -test("pending nudge cleared when suppressed — threshold resets to full", () => { +test("pending nudge preserved when all-protected — no loop", () => { const state = createSessionState() state.modelContextLimit = 1_000_000 state.messageIds.byRawId.set("a1", "m00001") @@ -952,8 +951,8 @@ test("pending nudge cleared when suppressed — threshold resets to full", () => assert.equal(state.nudges.shouldInjectThisTurn, false, "nudge suppressed — all protected") assert.equal( state.nudges.lastNudgeShownTokens, - undefined, - "pending nudge cleared — threshold resets to full (not halved) for next check", + 200_000, + "pending nudge baseline preserved — prevents loop (stale fallback → huge growth → re-fire)", ) assert.equal( state.nudges.lastPerMessageNudgeTokens, @@ -962,6 +961,55 @@ test("pending nudge cleared when suppressed — threshold resets to full", () => ) }) +test("multi-turn: all-protected does not loop (lastNudgeShownTokens stable)", () => { + const state = createSessionState() + state.modelContextLimit = 1_000_000 + state.messageIds.byRawId.set("a1", "m00001") + state.messageIds.byRawId.set("a2", "m00002") + state.messageIds.byRawId.set("a3", "m00003") + + const config = buildConfig() + config.compress.protectedTools = ["skill"] + config.compress.maxContextLimit = 500_000 + config.compress.minContextLimit = 200_000 + + state.nudges.lastPerMessageNudgeTokens = 200_000 + + const protectedTurn = (id: string, inputTokens: number) => + assistantMsgWithTokens(id, "work", { input: inputTokens, output: 30_000 }, [ + { + id: `${id}-part`, messageID: id, sessionID: SID, + type: "tool" as const, tool: "skill", callID: `${id}-call`, + state: { status: "completed" as const, input: {}, output: "x".repeat(80_000) }, + }, + ]) + + // Turn 1: nudge suppressed (all protected) + injectCompressNudges(state, config, logger, [protectedTurn("a1", 225_000)], {} as any) + assert.equal(state.nudges.shouldInjectThisTurn, false, "turn 1: all protected, nudge suppressed") + assert.equal(state.nudges.lastNudgeShownTokens, undefined, "turn 1: no nudge shown yet") + + state.nudges.lastNudgeShownTokens = 225_000 + + // Turn 2: growth continues, still all-protected + injectCompressNudges(state, config, logger, [protectedTurn("a2", 230_000)], {} as any) + assert.equal(state.nudges.shouldInjectThisTurn, false, "turn 2: still all protected") + assert.equal( + state.nudges.lastNudgeShownTokens, + 225_000, + "turn 2: baseline preserved — NOT reset (prevents loop)", + ) + + // Turn 3: more growth, still all-protected + injectCompressNudges(state, config, logger, [protectedTurn("a3", 240_000)], {} as any) + assert.equal(state.nudges.shouldInjectThisTurn, false, "turn 3: still all protected") + assert.equal( + state.nudges.lastNudgeShownTokens, + 225_000, + "turn 3: baseline still preserved — no loop", + ) +}) + test("voluntary compress after suppression does not trigger proportional baseline adjustment", () => { const state = createSessionState() state.modelContextLimit = 1_000_000 diff --git a/tests/property-invariants.test.ts b/tests/property-invariants.test.ts index ef61f73..d50c834 100644 --- a/tests/property-invariants.test.ts +++ b/tests/property-invariants.test.ts @@ -398,12 +398,15 @@ test("INV4c: computeShouldNudge returns false when growth step not met and not o }) // ═══════════════════════════════════════════════════════════════════════ -// INV5: filterRecommendedRanges — suppressed ⟹ effective < growth threshold +// INV5: filterRecommendedRanges — always returns all input ranges // ═══════════════════════════════════════════════════════════════════════ -// Correctness property: when the filter suppresses all recommendations, -// the effective compressible tokens must be below the growth threshold. +// Issue #251 regression: filterRecommendedRanges used to suppress ranges +// below a context-relative threshold (5% of modelContextLimit). At 1M context +// this meant 50K+ of compressible content was never recommended. +// New invariant: ALL input ranges are always returned (no suppression). +// The minCompressRange backstop in range.ts handles garbage filtering. -test("INV5: filterRecommendedRanges suppressed implies effective below threshold", () => { +test("INV5: filterRecommendedRanges always returns all input ranges", () => { fc.assert( fc.property( fc.array( @@ -417,34 +420,17 @@ test("INV5: filterRecommendedRanges suppressed implies effective below threshold }), { minLength: 1, maxLength: 20 }, ), - arbModelLimit, - (ranges, modelContextLimit) => { - const growthRatio = 0.05 - const growthThreshold = modelContextLimit * growthRatio - const lastSegmentFloor = growthThreshold * 2 - + (ranges) => { const result = filterRecommendedRanges( ranges as CompressibleRange[], [], - { modelContextLimit, growthRatio }, + {}, + ) + assert.equal( + result.length, + ranges.length, + `Input has ${ranges.length} ranges but got ${result.length} — suppression should not occur`, ) - - if (result.length === 0) { - // Suppressed — verify effective compressible < threshold - const lastIndex = ranges.length - 1 - let effective = 0 - for (let i = 0; i < ranges.length; i++) { - if (i === lastIndex) { - effective += Math.max(0, ranges[i]!.tokens - lastSegmentFloor) - } else { - effective += ranges[i]!.tokens - } - } - assert.ok( - effective < growthThreshold, - `Suppressed but effective=${effective} >= threshold=${growthThreshold}`, - ) - } }, ), { numRuns: 300 }, diff --git a/tests/smart-nudge-gating.test.ts b/tests/smart-nudge-gating.test.ts index 01ca39c..9ead9fd 100644 --- a/tests/smart-nudge-gating.test.ts +++ b/tests/smart-nudge-gating.test.ts @@ -23,128 +23,67 @@ function makeProtected( return { startRef, endRef, count, tokens, tools } } -const OPTS_1M = { modelContextLimit: 1_000_000, growthRatio: 0.05 } -const OPTS_200K = { modelContextLimit: 200_000, growthRatio: 0.05 } +const OPTS = {} -test("last segment < 2x growth threshold (10% for 1M) excluded + suppressed", () => { +test("single range: returned with dangerous flag", () => { const ranges = [makeRange("m00001", "m00003", 3, 80_000)] - const result = filterRecommendedRanges(ranges, [], OPTS_1M) - assert.equal(result.length, 0, "80K < 100K floor → excluded, effective=0 < 50K gate") -}) - -test("last segment far above threshold: 300K, effective = 200K shown with dangerous", () => { - const ranges = [makeRange("m00001", "m00001", 1, 300_000)] - const result = filterRecommendedRanges(ranges, [], OPTS_1M) + const result = filterRecommendedRanges(ranges, [], OPTS) assert.equal(result.length, 1) assert.equal(result[0].dangerous, true) }) -test("single range at exactly 3x (150K): floor passes + gate passes", () => { - const ranges = [makeRange("m00001", "m00001", 1, 150_000)] - const result = filterRecommendedRanges(ranges, [], OPTS_1M) - assert.equal(result.length, 1) +test("small single range not suppressed (Issue #251 regression)", () => { + const ranges = [makeRange("m00001", "m00001", 1, 10_000)] + const result = filterRecommendedRanges(ranges, [], OPTS) + assert.equal(result.length, 1, "small ranges must not be suppressed") assert.equal(result[0].dangerous, true) }) -test("single range at 2x boundary (100K): floor passes but gate fails (effective=0)", () => { - const ranges = [makeRange("m00001", "m00001", 1, 100_000)] - const result = filterRecommendedRanges(ranges, [], OPTS_1M) - assert.equal(result.length, 0, "effective = max(0, 100K-100K) = 0 < 50K gate") -}) - -test("non-last range never gets dangerous flag, last range does", () => { - const ranges = [ - makeRange("m00001", "m00005", 5, 60_000), - makeRange("m00010", "m00015", 6, 150_000), - ] - const result = filterRecommendedRanges(ranges, [], OPTS_1M) - assert.equal(result.length, 2) - assert.equal(result[0].dangerous, undefined) - assert.equal(result[1].dangerous, true) -}) - -test("gate: single non-last range below growth threshold suppressed", () => { - const ranges = [makeRange("m00001", "m00005", 5, 30_000)] - const result = filterRecommendedRanges(ranges, [], OPTS_1M) - assert.equal(result.length, 0) -}) - -test("effective compressible: non-last 40K + last 80K (excluded) = 40K < 50K suppressed", () => { - const ranges = [ - makeRange("m00001", "m00005", 5, 40_000), - makeRange("m00006", "m00008", 3, 80_000), - ] - const result = filterRecommendedRanges(ranges, [], OPTS_1M) - assert.equal(result.length, 0) -}) - -test("effective compressible: non-last 60K + last 80K (excluded) = 60K >= 50K shown", () => { +test("multiple ranges: all shown, only last gets dangerous", () => { const ranges = [ - makeRange("m00001", "m00005", 5, 60_000), - makeRange("m00006", "m00008", 3, 80_000), + makeRange("m00001", "m00005", 5, 30_000), + makeRange("m00010", "m00015", 6, 20_000), + makeRange("m00020", "m00025", 6, 15_000), ] - const result = filterRecommendedRanges(ranges, [], OPTS_1M) - assert.equal(result.length, 1) - assert.equal(result[0].startRef, "m00001") + const result = filterRecommendedRanges(ranges, [], OPTS) + assert.equal(result.length, 3) assert.equal(result[0].dangerous, undefined) + assert.equal(result[1].dangerous, undefined) + assert.equal(result[2].dangerous, true) }) -test("effective compressible: non-last 40K + last 150K = 40K + 50K = 90K >= 50K shown", () => { +test("aggregate below old 5% threshold no longer suppresses (Issue #251)", () => { const ranges = [ - makeRange("m00001", "m00005", 5, 40_000), - makeRange("m00006", "m00010", 5, 150_000), + makeRange("m00001", "m00005", 5, 15_000), + makeRange("m00006", "m00008", 3, 10_000), ] - const result = filterRecommendedRanges(ranges, [], OPTS_1M) - assert.equal(result.length, 2) + const result = filterRecommendedRanges(ranges, [], OPTS) + assert.equal(result.length, 2, "25K total at 1M context must not be suppressed") assert.equal(result[0].dangerous, undefined) assert.equal(result[1].dangerous, true) }) -test("protected ranges do not affect filtering logic", () => { +test("tiny ranges (500 tokens) still shown — minCompressRange is the backstop", () => { const ranges = [ - makeRange("m00001", "m00005", 5, 60_000), - makeRange("m00006", "m00010", 5, 150_000), + makeRange("m00001", "m00002", 2, 300), + makeRange("m00003", "m00004", 2, 200), ] - const protectedRanges = [makeProtected("m00020", "m00030", 11, 300_000)] - const withoutProtected = filterRecommendedRanges(ranges, [], OPTS_1M) - const withProtected = filterRecommendedRanges(ranges, protectedRanges, OPTS_1M) - assert.deepEqual(withProtected, withoutProtected) -}) - -test("single small message as only range suppressed", () => { - const ranges = [makeRange("m00001", "m00001", 1, 10_000)] - const result = filterRecommendedRanges(ranges, [], OPTS_1M) - assert.equal(result.length, 0) + const result = filterRecommendedRanges(ranges, [], OPTS) + assert.equal(result.length, 2) }) test("empty input returns empty", () => { - const result = filterRecommendedRanges([], [], OPTS_1M) + const result = filterRecommendedRanges([], [], OPTS) assert.equal(result.length, 0) }) -test("modelContextLimit unknown: returns all with last marked dangerous", () => { +test("protected ranges do not affect filtering logic", () => { const ranges = [ - makeRange("m00001", "m00001", 1, 100), - makeRange("m00002", "m00005", 4, 200), + makeRange("m00001", "m00005", 5, 60_000), + makeRange("m00006", "m00010", 5, 50_000), ] - const result = filterRecommendedRanges(ranges, [], { - modelContextLimit: undefined, - growthRatio: 0.05, - }) - assert.equal(result.length, 2) - assert.equal(result[0].dangerous, undefined) - assert.equal(result[1].dangerous, true) -}) - -test("200K context: growth=10K, floor=20K. 35K → effective=15K >= 10K shown", () => { - const ranges = [makeRange("m00001", "m00001", 1, 35_000)] - const result = filterRecommendedRanges(ranges, [], OPTS_200K) - assert.equal(result.length, 1) - assert.equal(result[0].dangerous, true) -}) - -test("200K context: 25K → effective=5K < 10K suppressed", () => { - const ranges = [makeRange("m00001", "m00001", 1, 25_000)] - const result = filterRecommendedRanges(ranges, [], OPTS_200K) - assert.equal(result.length, 0) + const protectedRanges = [makeProtected("m00020", "m00030", 11, 300_000)] + const withoutProtected = filterRecommendedRanges(ranges, [], OPTS) + const withProtected = filterRecommendedRanges(ranges, protectedRanges, OPTS) + assert.deepEqual(withProtected, withoutProtected) })