diff --git a/extensions/memory-core/src/memory/qmd-manager.test.ts b/extensions/memory-core/src/memory/qmd-manager.test.ts index 8781b28c1eb..fd94a391745 100644 --- a/extensions/memory-core/src/memory/qmd-manager.test.ts +++ b/extensions/memory-core/src/memory/qmd-manager.test.ts @@ -3822,6 +3822,40 @@ describe("QmdMemoryManager", () => { await manager.close(); }); + it("does not store qmd embed backoff when the process clock is invalid", async () => { + cfg = { + ...cfg, + memory: { + backend: "qmd", + qmd: { + includeDefaultMemory: false, + searchMode: "query", + update: { + interval: "0s", + debounceMs: 0, + onBoot: false, + }, + paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }], + }, + }, + } as OpenClawConfig; + const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001); + const { manager } = await createManager({ mode: "status" }); + try { + ( + manager as unknown as { + noteEmbedFailure: (reason: string, err: unknown) => void; + } + ).noteEmbedFailure("manual", new Error("embed failed")); + } finally { + dateNowSpy.mockRestore(); + } + + const status = manager.status() as { custom?: { qmd?: { embedBackoffUntil?: number | null } } }; + expect(status.custom?.qmd?.embedBackoffUntil).toBeNull(); + await manager.close(); + }); + it("runs periodic embed maintenance even when regular update scheduling is disabled", async () => { vi.useFakeTimers(); cfg = { diff --git a/extensions/memory-core/src/memory/qmd-manager.ts b/extensions/memory-core/src/memory/qmd-manager.ts index 63da720ee10..a574c0d3b50 100644 --- a/extensions/memory-core/src/memory/qmd-manager.ts +++ b/extensions/memory-core/src/memory/qmd-manager.ts @@ -49,7 +49,11 @@ import { type ResolvedQmdConfig, type ResolvedQmdMcporterConfig, } from "openclaw/plugin-sdk/memory-core-host-engine-storage"; -import { addTimerTimeoutGraceMs } from "openclaw/plugin-sdk/number-runtime"; +import { + addTimerTimeoutGraceMs, + isFutureDateTimestampMs, + resolveExpiresAtMsFromDurationMs, +} from "openclaw/plugin-sdk/number-runtime"; import { localeLowercasePreservingWhitespace, normalizeLowercaseStringOrEmpty, @@ -1701,7 +1705,7 @@ export class QmdMemoryManager implements MemorySearchManager { return false; } const now = Date.now(); - if (this.embedBackoffUntil !== null && now < this.embedBackoffUntil) { + if (this.embedBackoffUntil !== null && isFutureDateTimestampMs(this.embedBackoffUntil)) { return false; } const embedIntervalMs = this.qmd.update.embedIntervalMs; @@ -1808,7 +1812,7 @@ export class QmdMemoryManager implements MemorySearchManager { QMD_EMBED_BACKOFF_MAX_MS, QMD_EMBED_BACKOFF_BASE_MS * 2 ** Math.max(0, this.embedFailureCount - 1), ); - this.embedBackoffUntil = Date.now() + delayMs; + this.embedBackoffUntil = resolveExpiresAtMsFromDurationMs(delayMs) ?? null; log.warn( `qmd embed failed (${reason}): ${String(err)}; backing off for ${Math.ceil(delayMs / 1000)}s`, );