From 23e6c78c2264aaf300ce3a6dcac856cb24222c2c Mon Sep 17 00:00:00 2001 From: jdcodes1 Date: Sat, 11 Apr 2026 21:15:58 -0400 Subject: [PATCH] feat: use LRU eviction for evaluation cache instead of FIFO On cache hit, move the entry to the end of Map iteration order so recently accessed entries survive eviction. Adds a test verifying the oldest-but-recently-accessed entry is kept while the true LRU entry is evicted. Co-Authored-By: Claude Opus 4.6 (1M context) --- Bouncer/src/background/pipeline.ts | 3 ++ Bouncer/tests/background/pipeline.test.ts | 48 +++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/Bouncer/src/background/pipeline.ts b/Bouncer/src/background/pipeline.ts index e10ba11..fdbfd0d 100644 --- a/Bouncer/src/background/pipeline.ts +++ b/Bouncer/src/background/pipeline.ts @@ -522,6 +522,9 @@ async function processBatch(): Promise { const cacheKey = generateCacheKey(item.post, imageUrls); if (evaluationCache.has(cacheKey)) { const cached = evaluationCache.get(cacheKey)!; + // LRU: move to end of Map iteration order so it's evicted last + evaluationCache.delete(cacheKey); + evaluationCache.set(cacheKey, cached); resolveWithDuplicates(batchTabId, item, { ...cached, cached: true }); inFlightBatches--; if (pendingEvaluations.length > 0) scheduleBatch(); diff --git a/Bouncer/tests/background/pipeline.test.ts b/Bouncer/tests/background/pipeline.test.ts index 9ee062f..da534bf 100644 --- a/Bouncer/tests/background/pipeline.test.ts +++ b/Bouncer/tests/background/pipeline.test.ts @@ -287,6 +287,54 @@ describe('processBatch re-queue on inference queue cleared', () => { }); }); +// ==================== LRU cache eviction ==================== + +describe('LRU cache eviction', () => { + it('recently accessed cache entries survive eviction', async () => { + const { evaluationCache } = await import('../../src/background/pipeline.js'); + + evaluationCache.clear(); + for (let i = 0; i < 500; i++) { + evaluationCache.set(`post-${i}`, { + shouldHide: false, + reasoning: 'test', + category: null, + rawResponse: null, + timestamp: Date.now(), + model: 'test', + cached: false, + }); + } + expect(evaluationCache.size).toBe(500); + + // Access post-0 (oldest) to mark it as recently used — simulating the LRU promotion + const oldest = evaluationCache.get('post-0')!; + evaluationCache.delete('post-0'); + evaluationCache.set('post-0', oldest); + + // Add a new entry — should evict post-1 (now the LRU), NOT post-0 + evaluationCache.set('post-new', { + shouldHide: false, + reasoning: 'new', + category: null, + rawResponse: null, + timestamp: Date.now(), + model: 'test', + cached: false, + }); + + // Evict like pipeline does + if (evaluationCache.size > 500) { + const firstKey = evaluationCache.keys().next().value; + if (firstKey !== undefined) evaluationCache.delete(firstKey); + } + + expect(evaluationCache.has('post-0')).toBe(true); + expect(evaluationCache.has('post-1')).toBe(false); + expect(evaluationCache.has('post-new')).toBe(true); + }); +}); + // Note: The no-model-configured path (empty selectedModel -> no_api_key error) is only // reachable in no-Imbue builds where DEFAULT_MODEL is ''. Since DEFAULT_MODEL is computed // at module load time from the build-time constant HAS_IMBUE_BACKEND, it can't be varied