fix(memory): bound native chat enrichment cache

This commit is contained in:
m4air
2026-09-19 16:03:35 -07:00
parent a220b321c6
commit 810ae3268c
2 changed files with 42 additions and 2 deletions
@@ -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(
@@ -19,6 +19,21 @@ type CatalogEnrichmentEntry = {
}
const enrichmentByAgentHost = new Map<string, CatalogEnrichmentEntry>()
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
}