diff --git a/src/main/ai-vault/cached-session-list.ts b/src/main/ai-vault/cached-session-list.ts index 5eeb71995e3..5f5fc135819 100644 --- a/src/main/ai-vault/cached-session-list.ts +++ b/src/main/ai-vault/cached-session-list.ts @@ -70,8 +70,12 @@ export async function listAiVaultSessions(args?: AiVaultListArgs): Promise { - inflightKey = null - inflightList = null + // Only clear tracking if it still refers to this request: a concurrent + // different-key scan may have replaced it and must stay dedupable. + if (inflightKey === key) { + inflightKey = null + inflightList = null + } }) return inflightList } diff --git a/src/main/runtime/rpc/methods/ai-vault.test.ts b/src/main/runtime/rpc/methods/ai-vault.test.ts index 8fa5795aeed..b136c231d0d 100644 --- a/src/main/runtime/rpc/methods/ai-vault.test.ts +++ b/src/main/runtime/rpc/methods/ai-vault.test.ts @@ -129,6 +129,33 @@ describe('aiVault.listSessions handler + shared cache', () => { expect(scanAiVaultSessions).toHaveBeenCalledTimes(1) }) + it('keeps a newer different-key scan dedupable after an older scan resolves', async () => { + // Why: the resolving scan's cleanup must not clear tracking a concurrent + // different-key scan replaced, or re-requests start a duplicate rescan. + const deferreds: ((result: AiVaultListResult) => void)[] = [] + scanAiVaultSessions.mockImplementation( + () => new Promise((resolve) => deferreds.push(resolve)) + ) + // The scanner is invoked a microtask after the call (WSL-home await), so + // wait for each deferred to register before driving resolution order. + const scanA = listAiVaultSessions({ limit: 100 }) + await vi.waitFor(() => expect(deferreds).toHaveLength(1)) + const scanB = listAiVaultSessions({ limit: 200 }) + await vi.waitFor(() => expect(deferreds).toHaveLength(2)) + deferreds[0]?.(makeResult()) + await scanA + // Re-request key B while its first scan is still pending: must dedupe. + const scanBAgain = listAiVaultSessions({ limit: 200 }) + // Flush a macrotask so that a wrongly-started duplicate scan would reach the + // mock (it fires a microtask after the WSL-home await): guarded stays 2, a + // reverted guard reads 3, so this assertion — not a Promise.all hang — pins + // the fix. + await new Promise((resolve) => setTimeout(resolve)) + expect(scanAiVaultSessions).toHaveBeenCalledTimes(2) + deferreds[1]?.(makeResult()) + await Promise.all([scanB, scanBAgain]) + }) + it('restamps the shared cached result as the addressed runtime host', async () => { scanAiVaultSessions.mockResolvedValue({ sessions: [makeSession()],