diff --git a/src/memory/qmd-manager.test.ts b/src/memory/qmd-manager.test.ts index 0f08affe6a0..1308408b3cb 100644 --- a/src/memory/qmd-manager.test.ts +++ b/src/memory/qmd-manager.test.ts @@ -824,6 +824,63 @@ describe("QmdMemoryManager", () => { await manager.close(); }); + it("rebuilds managed collections once when qmd update fails with null-byte ENOENT", async () => { + cfg = { + ...cfg, + memory: { + backend: "qmd", + qmd: { + includeDefaultMemory: true, + update: { interval: "0s", debounceMs: 0, onBoot: false }, + paths: [], + }, + }, + } as OpenClawConfig; + + let updateCalls = 0; + spawnMock.mockImplementation((_cmd: string, args: string[]) => { + if (args[0] === "update") { + updateCalls += 1; + const child = createMockChild({ autoClose: false }); + if (updateCalls === 1) { + emitAndClose( + child, + "stderr", + "ENOENT: no such file or directory, open '/tmp/workspace/sessions\\u0000'", + 1, + ); + return child; + } + queueMicrotask(() => { + child.closeWith(0); + }); + return child; + } + return createMockChild(); + }); + + const { manager } = await createManager({ mode: "status" }); + await expect(manager.sync({ reason: "manual" })).resolves.toBeUndefined(); + + const removeCalls = spawnMock.mock.calls + .map((call: unknown[]) => call[1] as string[]) + .filter((args: string[]) => args[0] === "collection" && args[1] === "remove") + .map((args) => args[2]); + const addCalls = spawnMock.mock.calls + .map((call: unknown[]) => call[1] as string[]) + .filter((args: string[]) => args[0] === "collection" && args[1] === "add") + .map((args) => args[args.indexOf("--name") + 1]); + + expect(updateCalls).toBe(2); + expect(removeCalls).toEqual(["memory-root-main", "memory-alt-main", "memory-dir-main"]); + expect(addCalls).toEqual(["memory-root-main", "memory-alt-main", "memory-dir-main"]); + expect(logWarnMock).toHaveBeenCalledWith( + expect.stringContaining("suspected null-byte collection metadata"), + ); + + await manager.close(); + }); + it("rebuilds managed collections once when qmd update hits duplicate document constraint", async () => { cfg = { ...cfg, diff --git a/src/memory/qmd-manager.ts b/src/memory/qmd-manager.ts index 46a80156677..c941c6ed570 100644 --- a/src/memory/qmd-manager.ts +++ b/src/memory/qmd-manager.ts @@ -650,7 +650,10 @@ export class QmdMemoryManager implements MemorySearchManager { const message = err instanceof Error ? err.message : String(err); const lower = message.toLowerCase(); return ( - (lower.includes("enotdir") || lower.includes("not a directory")) && + (lower.includes("enotdir") || + lower.includes("not a directory") || + lower.includes("enoent") || + lower.includes("no such file")) && NUL_MARKER_RE.test(message) ); }