From 810ae3268c20af32d1e7bf8ee768d54356deeacd Mon Sep 17 00:00:00 2001 From: m4air Date: Sat, 19 Sep 2026 16:03:35 -0700 Subject: [PATCH] fix(memory): bound native chat enrichment cache --- ...ive-chat-session-option-enrichment.test.ts | 18 +++++++++++++ .../native-chat-session-option-enrichment.ts | 26 +++++++++++++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/native-chat/native-chat-session-option-enrichment.test.ts b/src/renderer/src/components/native-chat/native-chat-session-option-enrichment.test.ts index b5650cc16bc..6e574132764 100644 --- a/src/renderer/src/components/native-chat/native-chat-session-option-enrichment.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-session-option-enrichment.test.ts @@ -7,6 +7,8 @@ import { import { clearNativeChatModelEnrichmentForTests, ensureNativeChatModelEnrichment, + getNativeChatModelEnrichmentEntryCountForTests, + NATIVE_CHAT_MODEL_ENRICHMENT_MAX_ENTRIES, readNativeChatEnrichedModels, resolveNativeChatLaunchSessionOptions, subscribeNativeChatEnrichedModels @@ -27,6 +29,22 @@ describe('native chat session option enrichment', () => { mocks.discoverRuntimeCommitMessageModels.mockReset() }) + it('bounds settled host enrichment entries', async () => { + for (let index = 0; index < NATIVE_CHAT_MODEL_ENRICHMENT_MAX_ENTRIES + 4; index += 1) { + ensureNativeChatModelEnrichment({ + agent: 'cursor', + hostKey: `ssh:${index}`, + discover: async () => [] + }) + } + await Promise.resolve() + await Promise.resolve() + + expect(getNativeChatModelEnrichmentEntryCountForTests()).toBe( + NATIVE_CHAT_MODEL_ENRICHMENT_MAX_ENTRIES + ) + }) + it('keeps reads synchronous while one host-scoped probe is in flight', async () => { let resolveDiscovery: ((models: CatalogModel[]) => void) | undefined const discover = vi.fn( diff --git a/src/renderer/src/components/native-chat/native-chat-session-option-enrichment.ts b/src/renderer/src/components/native-chat/native-chat-session-option-enrichment.ts index 1bb49699050..5254854dada 100644 --- a/src/renderer/src/components/native-chat/native-chat-session-option-enrichment.ts +++ b/src/renderer/src/components/native-chat/native-chat-session-option-enrichment.ts @@ -19,6 +19,21 @@ type CatalogEnrichmentEntry = { } const enrichmentByAgentHost = new Map() +export const NATIVE_CHAT_MODEL_ENRICHMENT_MAX_ENTRIES = 256 + +function retainEnrichmentEntry(key: string, entry: CatalogEnrichmentEntry): void { + enrichmentByAgentHost.delete(key) + enrichmentByAgentHost.set(key, entry) + while (enrichmentByAgentHost.size > NATIVE_CHAT_MODEL_ENRICHMENT_MAX_ENTRIES) { + const evictable = [...enrichmentByAgentHost].find( + ([, candidate]) => candidate.listeners.size === 0 && candidate.state !== 'pending' + ) + if (!evictable) { + return + } + enrichmentByAgentHost.delete(evictable[0]) + } +} function enrichmentKey(agent: AgentType, hostKey: string): string { return JSON.stringify([agent, hostKey]) @@ -45,7 +60,7 @@ export function subscribeNativeChatEnrichedModels( listeners: new Set<(models: CatalogModel[]) => void>() } entry.listeners.add(listener) - enrichmentByAgentHost.set(key, entry) + retainEnrichmentEntry(key, entry) return () => entry.listeners.delete(listener) } @@ -90,7 +105,7 @@ export function ensureNativeChatModelEnrichment(args: { listeners: new Set() } entry.state = 'pending' - enrichmentByAgentHost.set(key, entry) + retainEnrichmentEntry(key, entry) // Why: model discovery must never delay rendering or launching; the seed is // immediately usable while this once-per-host probe runs in the background. @@ -98,6 +113,7 @@ export function ensureNativeChatModelEnrichment(args: { .discover() .then((discovered) => { entry.state = 'settled' + retainEnrichmentEntry(key, entry) if (!discovered || discovered.length === 0) { return } @@ -113,9 +129,15 @@ export function ensureNativeChatModelEnrichment(args: { }) .catch(() => { entry.state = 'settled' + retainEnrichmentEntry(key, entry) }) } export function clearNativeChatModelEnrichmentForTests(): void { enrichmentByAgentHost.clear() } + +/** @internal - exposed for leak-regression tests only. */ +export function getNativeChatModelEnrichmentEntryCountForTests(): number { + return enrichmentByAgentHost.size +}