mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:50:42 +00:00
fix(memory-core): fix macOS chokidar glob issue by watching memory dir directly (#64711)
* fix(memory-core): fix macOS chokidar glob issue by watching memory dir directly * fix(memory-core): ignore non-markdown memory watch churn * fix(memory-core): allow multimodal watch events * test(memory-core): type watcher ignore callback --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -90,12 +90,29 @@ const IGNORED_MEMORY_WATCH_DIR_NAMES = new Set([
|
||||
|
||||
const log = createSubsystemLogger("memory");
|
||||
|
||||
function shouldIgnoreMemoryWatchPath(watchPath: string): boolean {
|
||||
function shouldIgnoreMemoryWatchPath(
|
||||
watchPath: string,
|
||||
stats?: { isDirectory?: () => boolean },
|
||||
multimodalSettings?: ResolvedMemorySearchConfig["multimodal"],
|
||||
): boolean {
|
||||
const normalized = path.normalize(watchPath);
|
||||
const parts = normalized
|
||||
.split(path.sep)
|
||||
.map((segment) => normalizeLowercaseStringOrEmpty(segment));
|
||||
return parts.some((segment) => IGNORED_MEMORY_WATCH_DIR_NAMES.has(segment));
|
||||
if (parts.some((segment) => IGNORED_MEMORY_WATCH_DIR_NAMES.has(segment))) {
|
||||
return true;
|
||||
}
|
||||
if (stats?.isDirectory?.()) {
|
||||
return false;
|
||||
}
|
||||
const extension = normalizeLowercaseStringOrEmpty(path.extname(normalized));
|
||||
if (extension.length === 0 || extension === ".md") {
|
||||
return false;
|
||||
}
|
||||
if (!multimodalSettings) {
|
||||
return true;
|
||||
}
|
||||
return classifyMemoryMultimodalPath(normalized, multimodalSettings) === null;
|
||||
}
|
||||
|
||||
export function runDetachedMemorySync(sync: () => Promise<void>, reason: "interval" | "watch") {
|
||||
@@ -345,7 +362,7 @@ export abstract class MemoryManagerSyncOps {
|
||||
const watchPaths = new Set<string>([
|
||||
path.join(this.workspaceDir, "MEMORY.md"),
|
||||
path.join(this.workspaceDir, "memory.md"),
|
||||
path.join(this.workspaceDir, "memory", "**", "*.md"),
|
||||
path.join(this.workspaceDir, "memory"),
|
||||
]);
|
||||
const additionalPaths = normalizeExtraMemoryPaths(this.workspaceDir, this.settings.extraPaths);
|
||||
for (const entry of additionalPaths) {
|
||||
@@ -380,7 +397,8 @@ export abstract class MemoryManagerSyncOps {
|
||||
}
|
||||
this.watcher = chokidar.watch(Array.from(watchPaths), {
|
||||
ignoreInitial: true,
|
||||
ignored: (watchPath) => shouldIgnoreMemoryWatchPath(watchPath),
|
||||
ignored: (watchPath, stats) =>
|
||||
shouldIgnoreMemoryWatchPath(watchPath, stats, this.settings.multimodal),
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: this.settings.sync.watchDebounceMs,
|
||||
pollInterval: 100,
|
||||
|
||||
@@ -9,6 +9,8 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi }
|
||||
import type { MemoryIndexManager } from "./index.js";
|
||||
import { registerBuiltInMemoryEmbeddingProviders } from "./provider-adapters.js";
|
||||
|
||||
type WatchIgnoredFn = (watchPath: string, stats?: { isDirectory?: () => boolean }) => boolean;
|
||||
|
||||
const { watchMock } = vi.hoisted(() => ({
|
||||
watchMock: vi.fn(() => ({
|
||||
on: vi.fn(),
|
||||
@@ -123,7 +125,7 @@ describe("memory watcher config", () => {
|
||||
manager = result.manager as unknown as MemoryIndexManager;
|
||||
}
|
||||
|
||||
it("watches markdown globs and ignores dependency directories", async () => {
|
||||
it("watches the memory directory and ignores non-markdown churn", async () => {
|
||||
await setupWatcherWorkspace({ name: "notes.md", contents: "hello" });
|
||||
const cfg = createWatcherConfig();
|
||||
|
||||
@@ -138,20 +140,25 @@ describe("memory watcher config", () => {
|
||||
expect.arrayContaining([
|
||||
path.join(workspaceDir, "MEMORY.md"),
|
||||
path.join(workspaceDir, "memory.md"),
|
||||
path.join(workspaceDir, "memory", "**", "*.md"),
|
||||
path.join(workspaceDir, "memory"),
|
||||
path.join(extraDir, "**", "*.md"),
|
||||
]),
|
||||
);
|
||||
expect(options.ignoreInitial).toBe(true);
|
||||
expect(options.awaitWriteFinish).toEqual({ stabilityThreshold: 25, pollInterval: 100 });
|
||||
|
||||
const ignored = options.ignored as ((watchPath: string) => boolean) | undefined;
|
||||
const ignored = options.ignored as WatchIgnoredFn | undefined;
|
||||
expect(ignored).toBeTypeOf("function");
|
||||
expect(ignored?.(path.join(workspaceDir, "memory", "node_modules", "pkg", "index.md"))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(ignored?.(path.join(workspaceDir, "memory", ".venv", "lib", "python.md"))).toBe(true);
|
||||
expect(ignored?.(path.join(workspaceDir, "memory", "project", "notes.tmp"))).toBe(true);
|
||||
expect(ignored?.(path.join(workspaceDir, "memory", "project", "notes.json"))).toBe(true);
|
||||
expect(ignored?.(path.join(workspaceDir, "memory", "project", "notes.md"))).toBe(false);
|
||||
expect(
|
||||
ignored?.(path.join(workspaceDir, "memory", "project"), { isDirectory: () => true }),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("watches multimodal extensions with case-insensitive globs", async () => {
|
||||
@@ -166,7 +173,7 @@ describe("memory watcher config", () => {
|
||||
await expectWatcherManager(cfg);
|
||||
|
||||
expect(watchMock).toHaveBeenCalledTimes(1);
|
||||
const [watchedPaths] = watchMock.mock.calls[0] as unknown as [
|
||||
const [watchedPaths, options] = watchMock.mock.calls[0] as unknown as [
|
||||
string[],
|
||||
Record<string, unknown>,
|
||||
];
|
||||
@@ -176,5 +183,11 @@ describe("memory watcher config", () => {
|
||||
path.join(extraDir, "**", "*.[wW][aA][vV]"),
|
||||
]),
|
||||
);
|
||||
|
||||
const ignored = options.ignored as WatchIgnoredFn | undefined;
|
||||
expect(ignored).toBeTypeOf("function");
|
||||
expect(ignored?.(path.join(extraDir, "nested", "PHOTO.PNG"))).toBe(false);
|
||||
expect(ignored?.(path.join(extraDir, "nested", "voice.WAV"))).toBe(false);
|
||||
expect(ignored?.(path.join(extraDir, "nested", "metadata.json"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user