fix: handle ENOSPC file watcher errors gracefully (#73357)

Merged via squash.

Prepared head SHA: ce2dd6ed3e
Co-authored-by: solodmd <51304754+solodmd@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
solodmd
2026-05-03 16:42:38 +08:00
committed by GitHub
parent 4781b46056
commit d1365fef16
5 changed files with 230 additions and 6 deletions

View File

@@ -446,6 +446,12 @@ export abstract class MemoryManagerSyncOps {
this.watcher.on("change", markDirty);
this.watcher.on("unlink", markDirty);
this.watcher.on("unlinkDir", markDirty);
this.watcher.on("error", (err) => {
// File watcher errors (e.g., ENOSPC) should not crash the gateway.
// Log the error and continue - memory search still works without auto-sync.
const message = err instanceof Error ? err.message : String(err);
log.warn(`memory watcher error: ${message}`);
});
}
protected ensureSessionListener() {

View File

@@ -9,9 +9,9 @@ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vites
type WatchIgnoredFn = (watchPath: string, stats?: { isDirectory?: () => boolean }) => boolean;
const { createdWatchers, watchMock } = vi.hoisted(() => {
type WatchEvent = "add" | "change" | "unlink" | "unlinkDir";
type WatchCallback = () => void;
const { createdWatchers, memoryLoggerWarn, watchMock } = vi.hoisted(() => {
type WatchEvent = "add" | "change" | "unlink" | "unlinkDir" | "error";
type WatchCallback = (value?: unknown) => void;
function createMockWatcher() {
const handlers = new Map<WatchEvent, WatchCallback[]>();
const watcher = {
@@ -20,9 +20,9 @@ const { createdWatchers, watchMock } = vi.hoisted(() => {
return watcher;
}),
close: vi.fn(async () => undefined),
emit: (event: WatchEvent) => {
emit: (event: WatchEvent, value?: unknown) => {
for (const callback of handlers.get(event) ?? []) {
callback();
callback(value);
}
},
};
@@ -31,6 +31,7 @@ const { createdWatchers, watchMock } = vi.hoisted(() => {
const watchers: Array<ReturnType<typeof createMockWatcher>> = [];
const result = {
createdWatchers: watchers,
memoryLoggerWarn: vi.fn(),
watchMock: vi.fn(() => {
const watcher = createMockWatcher();
watchers.push(watcher);
@@ -42,6 +43,18 @@ const { createdWatchers, watchMock } = vi.hoisted(() => {
return result;
});
vi.mock("openclaw/plugin-sdk/memory-core-host-engine-foundation", async (importOriginal) => {
const actual =
await importOriginal<typeof import("openclaw/plugin-sdk/memory-core-host-engine-foundation")>();
return {
...actual,
createSubsystemLogger: (subsystem: string) => ({
...actual.createSubsystemLogger(subsystem),
warn: memoryLoggerWarn,
}),
};
});
vi.mock("./sqlite-vec.js", () => ({
loadSqliteVecExtension: async () => ({ ok: false, error: "sqlite-vec disabled in tests" }),
}));
@@ -246,4 +259,16 @@ describe("memory watcher config", () => {
expect(syncSpy).toHaveBeenCalledWith({ reason: "watch" });
},
);
it("attaches a logging non-throwing watcher error listener", async () => {
await setupWatcherWorkspace({ name: "notes.md", contents: "hello" });
const cfg = createWatcherConfig();
await expectWatcherManager(cfg);
const watcher = createdWatchers[0];
expect(watcher?.on).toHaveBeenCalledWith("error", expect.any(Function));
expect(() => watcher?.emit("error", new Error("watcher error: ENOSPC"))).not.toThrow();
expect(memoryLoggerWarn).toHaveBeenCalledWith("memory watcher error: watcher error: ENOSPC");
});
});