@@ -12,7 +12,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi }
1212import "./test-runtime-mocks.js" ;
1313import type { MemoryIndexManager } from "./index.js" ;
1414import { closeAllMemorySearchManagers , getMemorySearchManager } from "./index.js" ;
15- import { EMBEDDING_PROBE_CACHE_TTL_MS } from "./manager.js" ;
15+ import { closeMemoryIndexManagersForAgent , EMBEDDING_PROBE_CACHE_TTL_MS } from "./manager.js" ;
1616import {
1717 DEFAULT_LOCAL_MODEL ,
1818 registerBuiltInMemoryEmbeddingProviders ,
@@ -28,6 +28,9 @@ afterAll(() => {
2828
2929let embedBatchCalls = 0 ;
3030let embedBatchInputCalls = 0 ;
31+ let providerCloseCalls = 0 ;
32+ let providerCloseFailuresRemaining = 0 ;
33+ let providerCloseGate : Promise < void > | null = null ;
3134let providerCalls : Array < { provider ?: string ; model ?: string ; outputDimensionality ?: number } > = [ ] ;
3235let forceNoProvider = false ;
3336
@@ -65,6 +68,14 @@ vi.mock("./embeddings.js", () => {
6568 provider : {
6669 id : providerId ,
6770 model,
71+ close : async ( ) => {
72+ providerCloseCalls += 1 ;
73+ await providerCloseGate ;
74+ if ( providerCloseFailuresRemaining > 0 ) {
75+ providerCloseFailuresRemaining -= 1 ;
76+ throw new Error ( "provider close failed" ) ;
77+ }
78+ } ,
6879 embedQuery : async ( text : string ) => embedText ( text ) ,
6980 embedBatch : async ( texts : string [ ] ) => {
7081 embedBatchCalls += 1 ;
@@ -188,6 +199,9 @@ describe("memory index", () => {
188199 registerBuiltInMemoryEmbeddingProviders ( { registerMemoryEmbeddingProvider : registerAdapter } ) ;
189200 embedBatchCalls = 0 ;
190201 embedBatchInputCalls = 0 ;
202+ providerCloseCalls = 0 ;
203+ providerCloseFailuresRemaining = 0 ;
204+ providerCloseGate = null ;
191205 providerCalls = [ ] ;
192206 forceNoProvider = false ;
193207
@@ -354,6 +368,96 @@ describe("memory index", () => {
354368 }
355369 } ) ;
356370
371+ it ( "closes embedding providers when memory index managers close" , async ( ) => {
372+ const cfg = createCfg ( {
373+ storePath : indexMainPath ,
374+ hybrid : { enabled : true , vectorWeight : 0.5 , textWeight : 0.5 } ,
375+ } ) ;
376+ const manager = await getFreshManager ( cfg ) ;
377+
378+ await manager . probeEmbeddingAvailability ( ) ;
379+ expect ( providerCloseCalls ) . toBe ( 0 ) ;
380+
381+ await manager . close ( ) ;
382+ await manager . close ( ) ;
383+
384+ expect ( providerCloseCalls ) . toBe ( 1 ) ;
385+ } ) ;
386+
387+ it ( "closes embedding providers before waiting for pending sync to settle" , async ( ) => {
388+ const cfg = createCfg ( {
389+ storePath : indexMainPath ,
390+ hybrid : { enabled : true , vectorWeight : 0.5 , textWeight : 0.5 } ,
391+ } ) ;
392+ const manager = await getFreshManager ( cfg ) ;
393+ await manager . probeEmbeddingAvailability ( ) ;
394+ let resolveSync : ( ) => void = ( ) => { } ;
395+ ( manager as unknown as { syncing : Promise < void > } ) . syncing = new Promise < void > ( ( resolve ) => {
396+ resolveSync = resolve ;
397+ } ) ;
398+
399+ const closePromise = manager . close ( ) ;
400+ await vi . waitFor ( ( ) => {
401+ expect ( providerCloseCalls ) . toBe ( 1 ) ;
402+ } ) ;
403+ let closeSettled = false ;
404+ void closePromise . then ( ( ) => {
405+ closeSettled = true ;
406+ } ) ;
407+ await Promise . resolve ( ) ;
408+
409+ expect ( closeSettled ) . toBe ( false ) ;
410+ resolveSync ( ) ;
411+ await closePromise ;
412+ } ) ;
413+
414+ it ( "evicts scoped memory index managers before close settles" , async ( ) => {
415+ let releaseProviderClose : ( ) => void = ( ) => { } ;
416+ providerCloseGate = new Promise < void > ( ( resolve ) => {
417+ releaseProviderClose = resolve ;
418+ } ) ;
419+ const cfg = createCfg ( {
420+ storePath : indexMainPath ,
421+ hybrid : { enabled : true , vectorWeight : 0.5 , textWeight : 0.5 } ,
422+ } ) ;
423+ const first = requireManager ( await getMemorySearchManager ( { cfg, agentId : "main" } ) ) ;
424+ managersForCleanup . add ( first ) ;
425+ await first . probeEmbeddingAvailability ( ) ;
426+ const closePromise = closeMemoryIndexManagersForAgent ( { cfg, agentId : "main" } ) ;
427+ let second : MemoryIndexManager | null = null ;
428+ try {
429+ await vi . waitFor ( ( ) => {
430+ expect ( providerCloseCalls ) . toBe ( 1 ) ;
431+ } ) ;
432+
433+ second = requireManager ( await getMemorySearchManager ( { cfg, agentId : "main" } ) ) ;
434+ managersForCleanup . add ( second ) ;
435+ expect ( second ) . not . toBe ( first ) ;
436+ } finally {
437+ releaseProviderClose ( ) ;
438+ providerCloseGate = null ;
439+ }
440+ await closePromise ;
441+
442+ const third = requireManager ( await getMemorySearchManager ( { cfg, agentId : "main" } ) ) ;
443+ managersForCleanup . add ( third ) ;
444+ expect ( third ) . toBe ( second ) ;
445+ } ) ;
446+
447+ it ( "retries embedding provider close before releasing the manager" , async ( ) => {
448+ providerCloseFailuresRemaining = 1 ;
449+ const cfg = createCfg ( {
450+ storePath : indexMainPath ,
451+ hybrid : { enabled : true , vectorWeight : 0.5 , textWeight : 0.5 } ,
452+ } ) ;
453+ const manager = await getFreshManager ( cfg ) ;
454+
455+ await manager . probeEmbeddingAvailability ( ) ;
456+ await manager . close ( ) ;
457+
458+ expect ( providerCloseCalls ) . toBe ( 2 ) ;
459+ } ) ;
460+
357461 it ( "indexes multimodal image and audio files from extra paths with Gemini structured inputs" , async ( ) => {
358462 const mediaDir = path . join ( workspaceDir , "media-memory" ) ;
359463 await fs . mkdir ( mediaDir , { recursive : true } ) ;
0 commit comments