Skip to content

Commit c156fd5

Browse files
committed
refactor(memory-core): pair native memory watchers
1 parent 321c770 commit c156fd5

2 files changed

Lines changed: 51 additions & 71 deletions

File tree

extensions/memory-core/src/memory/manager-sync-ops.ts

Lines changed: 50 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ const log = createSubsystemLogger("memory");
108108
const TEST_MEMORY_WATCH_FACTORY_KEY = Symbol.for("openclaw.test.memoryWatchFactory");
109109
const 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+
111117
function 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

extensions/memory-core/src/memory/manager.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -955,26 +955,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
955955
await this.watcher.close();
956956
this.watcher = null;
957957
}
958-
if (this.nativeMemoryWatchers.length > 0) {
959-
for (const w of this.nativeMemoryWatchers) {
960-
try {
961-
w.close();
962-
} catch {
963-
// ignore close failures
964-
}
965-
}
966-
this.nativeMemoryWatchers = [];
967-
}
968-
if (this.nativeMemoryParentWatchers.length > 0) {
969-
for (const w of this.nativeMemoryParentWatchers) {
970-
try {
971-
w.close();
972-
} catch {
973-
// ignore close failures
974-
}
975-
}
976-
this.nativeMemoryParentWatchers = [];
977-
}
958+
this.closeNativeMemoryWatchPairs();
978959
if (this.sessionUnsubscribe) {
979960
this.sessionUnsubscribe();
980961
this.sessionUnsubscribe = null;

0 commit comments

Comments
 (0)