@@ -108,6 +108,12 @@ const log = createSubsystemLogger("memory");
108108const TEST_MEMORY_WATCH_FACTORY_KEY = Symbol . for ( "openclaw.test.memoryWatchFactory" ) ;
109109const TEST_MEMORY_NATIVE_WATCH_FACTORY_KEY = Symbol . for ( "openclaw.test.memoryNativeWatchFactory" ) ;
110110
111+ type NativeMemoryWatchPair = {
112+ dir : string ;
113+ main : fsSync . FSWatcher ;
114+ parent : fsSync . FSWatcher | null ;
115+ } ;
116+
111117function resolveMemoryWatchFactory ( ) : typeof chokidar . watch {
112118 if ( process . env . VITEST === "true" || process . env . NODE_ENV === "test" ) {
113119 const override = ( globalThis as Record < PropertyKey , unknown > ) [ TEST_MEMORY_WATCH_FACTORY_KEY ] ;
@@ -208,12 +214,7 @@ export abstract class MemoryManagerSyncOps {
208214 } = { enabled : false , available : false } ;
209215 protected vectorReady : Promise < boolean > | null = null ;
210216 protected watcher : FSWatcher | null = null ;
211- protected nativeMemoryWatchers : fsSync . FSWatcher [ ] = [ ] ;
212- // Non-recursive parent-directory watchers paired with `nativeMemoryWatchers`,
213- // used to detect root-directory replacement (`rm -rf memory && mkdir memory`)
214- // and reattach the main native watcher on the new inode. See
215- // attachNativeMemoryWatchForDir() for the lifecycle.
216- protected nativeMemoryParentWatchers : fsSync . FSWatcher [ ] = [ ] ;
217+ private nativeMemoryWatchPairs : NativeMemoryWatchPair [ ] = [ ] ;
217218 protected watchTimer : NodeJS . Timeout | null = null ;
218219 protected sessionWatchTimer : NodeJS . Timeout | null = null ;
219220 protected sessionUnsubscribe : ( ( ) => void ) | null = null ;
@@ -450,7 +451,7 @@ export abstract class MemoryManagerSyncOps {
450451 if ( ! this . sources . has ( "memory" ) || ! this . settings . sync . watch ) {
451452 return ;
452453 }
453- if ( this . watcher || this . nativeMemoryWatchers . length > 0 ) {
454+ if ( this . watcher || this . nativeMemoryWatchPairs . length > 0 ) {
454455 // Already initialized — preserve idempotence.
455456 return ;
456457 }
@@ -588,30 +589,12 @@ export abstract class MemoryManagerSyncOps {
588589 ) ;
589590 return false ;
590591 }
591- // Forward-declared so the main watcher's error handler can also
592- // close the paired parent watcher when it dies — otherwise the
593- // parent could later reattach native coverage on top of an already-
594- // installed chokidar fallback (clawsweeper review [P3] 5df68c…).
595- let parentWatcherRef : fsSync . FSWatcher | null = null ;
592+ const pair : NativeMemoryWatchPair = { dir, main : mainWatcher , parent : null } ;
596593 mainWatcher . on ( "error" , ( err ) => {
597594 const message = err instanceof Error ? err . message : String ( err ) ;
598595 log . warn ( `memory native watcher error on ${ dir } : ${ message } ` ) ;
599596 // Per Node docs the FSWatcher is no longer usable after an error.
600- try {
601- mainWatcher . close ( ) ;
602- } catch {
603- // ignore close failures on already-broken watcher
604- }
605- this . removeNativeMemoryWatch ( mainWatcher ) ;
606- if ( parentWatcherRef ) {
607- try {
608- parentWatcherRef . close ( ) ;
609- } catch {
610- // ignore
611- }
612- this . removeNativeMemoryParentWatch ( parentWatcherRef ) ;
613- parentWatcherRef = null ;
614- }
597+ this . closeNativeMemoryWatchPair ( pair ) ;
615598 if ( this . closed ) {
616599 return ;
617600 }
@@ -622,7 +605,7 @@ export abstract class MemoryManagerSyncOps {
622605 markDirty ( ) ;
623606 this . attachMemoryChokidarFallback ( dir , markDirty ) ;
624607 } ) ;
625- this . nativeMemoryWatchers . push ( mainWatcher ) ;
608+ this . nativeMemoryWatchPairs . push ( pair ) ;
626609 // Non-recursive parent watcher: catches root-directory replacement so
627610 // we can reattach the main watcher on the new inode. Without this,
628611 // `rm -rf memory && mkdir memory` would leave the main watcher bound
@@ -654,19 +637,7 @@ export abstract class MemoryManagerSyncOps {
654637 // Root was replaced (or removed). Tear down the existing pair
655638 // and either reattach (if dir still exists) or fall back to
656639 // chokidar (if dir is gone).
657- try {
658- mainWatcher . close ( ) ;
659- } catch {
660- // ignore
661- }
662- this . removeNativeMemoryWatch ( mainWatcher ) ;
663- try {
664- parentWatcher . close ( ) ;
665- } catch {
666- // ignore
667- }
668- this . removeNativeMemoryParentWatch ( parentWatcher ) ;
669- parentWatcherRef = null ;
640+ this . closeNativeMemoryWatchPair ( pair ) ;
670641 if ( this . closed ) {
671642 return ;
672643 }
@@ -694,14 +665,13 @@ export abstract class MemoryManagerSyncOps {
694665 // ignore
695666 }
696667 this . removeNativeMemoryParentWatch ( parentWatcher ) ;
697- if ( parentWatcherRef === parentWatcher ) {
698- parentWatcherRef = null ;
668+ if ( pair . parent === parentWatcher ) {
669+ pair . parent = null ;
699670 }
700671 // Main watcher still alive — root-replacement detection is lost
701672 // but normal events still flow. No fallback needed.
702673 } ) ;
703- this . nativeMemoryParentWatchers . push ( parentWatcher ) ;
704- parentWatcherRef = parentWatcher ;
674+ pair . parent = parentWatcher ;
705675 } catch ( err ) {
706676 // Parent watcher couldn't start (e.g. parentDir not accessible).
707677 // The main watcher still works for non-replacement events; just
@@ -713,17 +683,46 @@ export abstract class MemoryManagerSyncOps {
713683 return true ;
714684 }
715685
716- private removeNativeMemoryWatch ( w : fsSync . FSWatcher ) : void {
717- const idx = this . nativeMemoryWatchers . indexOf ( w ) ;
718- if ( idx >= 0 ) {
719- this . nativeMemoryWatchers . splice ( idx , 1 ) ;
686+ private closeNativeMemoryWatchPair ( pair : NativeMemoryWatchPair ) : void {
687+ try {
688+ pair . main . close ( ) ;
689+ } catch {
690+ // ignore close failures
691+ }
692+ if ( pair . parent ) {
693+ try {
694+ pair . parent . close ( ) ;
695+ } catch {
696+ // ignore close failures
697+ }
698+ pair . parent = null ;
699+ }
700+ this . removeNativeMemoryWatchPair ( pair ) ;
701+ }
702+
703+ protected closeNativeMemoryWatchPairs ( ) : void {
704+ while ( this . nativeMemoryWatchPairs . length > 0 ) {
705+ const pair = this . nativeMemoryWatchPairs [ 0 ] ;
706+ if ( ! pair ) {
707+ return ;
708+ }
709+ this . closeNativeMemoryWatchPair ( pair ) ;
720710 }
721711 }
722712
723713 private removeNativeMemoryParentWatch ( w : fsSync . FSWatcher ) : void {
724- const idx = this . nativeMemoryParentWatchers . indexOf ( w ) ;
714+ for ( const pair of this . nativeMemoryWatchPairs ) {
715+ if ( pair . parent === w ) {
716+ pair . parent = null ;
717+ return ;
718+ }
719+ }
720+ }
721+
722+ private removeNativeMemoryWatchPair ( pair : NativeMemoryWatchPair ) : void {
723+ const idx = this . nativeMemoryWatchPairs . indexOf ( pair ) ;
725724 if ( idx >= 0 ) {
726- this . nativeMemoryParentWatchers . splice ( idx , 1 ) ;
725+ this . nativeMemoryWatchPairs . splice ( idx , 1 ) ;
727726 }
728727 }
729728
0 commit comments