diff --git a/extensions/active-memory/index.test.ts b/extensions/active-memory/index.test.ts index ccc130274a5..f37692e92f7 100644 --- a/extensions/active-memory/index.test.ts +++ b/extensions/active-memory/index.test.ts @@ -4130,6 +4130,50 @@ describe("active-memory plugin", () => { expect(cached?.summary).toBe("memory 1"); }); + it("drops cached active-memory results when the current clock is not a valid date timestamp", () => { + const nowSpy = vi.spyOn(Date, "now").mockReturnValue(1_700_000_000_000); + const cacheKey = testing.buildCacheKey({ + agentId: "main", + sessionKey: "agent:main:invalid-clock-cache", + query: "cache invalid clock prompt", + }); + testing.setCachedResult( + cacheKey, + { + status: "ok", + elapsedMs: 1, + rawReply: "memory", + summary: "memory", + }, + 15_000, + ); + + nowSpy.mockReturnValue(Number.NaN); + + expect(testing.getCachedResult(cacheKey)).toBeUndefined(); + }); + + it("does not cache active-memory results when the expiry timestamp would exceed the valid date range", () => { + vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_000); + const cacheKey = testing.buildCacheKey({ + agentId: "main", + sessionKey: "agent:main:overflow-cache", + query: "cache overflow prompt", + }); + testing.setCachedResult( + cacheKey, + { + status: "ok", + elapsedMs: 1, + rawReply: "memory", + summary: "memory", + }, + 15_000, + ); + + expect(testing.getCachedResult(cacheKey)).toBeUndefined(); + }); + it("skips recall after consecutive timeouts when circuit breaker trips (#74054)", async () => { const CONFIGURED_TIMEOUT_MS = 25; testing.setMinimumTimeoutMsForTests(1); diff --git a/extensions/active-memory/index.ts b/extensions/active-memory/index.ts index 0bbc9f79602..4fef112d89e 100644 --- a/extensions/active-memory/index.ts +++ b/extensions/active-memory/index.ts @@ -13,7 +13,11 @@ import { } from "openclaw/plugin-sdk/agent-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { closeActiveMemorySearchManager } from "openclaw/plugin-sdk/memory-host-search"; -import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime"; +import { + asDateTimestampMs, + parseStrictPositiveInteger, + resolveExpiresAtMsFromDurationMs, +} from "openclaw/plugin-sdk/number-runtime"; import { resolveLivePluginConfigObject, resolvePluginConfigObject, @@ -1360,7 +1364,12 @@ function getCachedResult(cacheKey: string): ActiveRecallResult | undefined { if (!cached) { return undefined; } - if (cached.expiresAt <= Date.now()) { + const now = asDateTimestampMs(Date.now()); + if ( + now === undefined || + asDateTimestampMs(cached.expiresAt) === undefined || + cached.expiresAt <= now + ) { activeRecallCache.delete(cacheKey); return undefined; } @@ -1368,19 +1377,27 @@ function getCachedResult(cacheKey: string): ActiveRecallResult | undefined { } function setCachedResult(cacheKey: string, result: ActiveRecallResult, ttlMs: number): void { - const now = Date.now(); + const rawNow = Date.now(); + const now = asDateTimestampMs(rawNow); if ( activeRecallCache.size >= DEFAULT_MAX_CACHE_ENTRIES || - now - lastActiveRecallCacheSweepAt >= CACHE_SWEEP_INTERVAL_MS + (now !== undefined && now - lastActiveRecallCacheSweepAt >= CACHE_SWEEP_INTERVAL_MS) ) { sweepExpiredCacheEntries(now); - lastActiveRecallCacheSweepAt = now; + if (now !== undefined) { + lastActiveRecallCacheSweepAt = now; + } + } + const expiresAt = resolveExpiresAtMsFromDurationMs(ttlMs, { nowMs: rawNow }); + if (expiresAt === undefined) { + activeRecallCache.delete(cacheKey); + return; } if (activeRecallCache.has(cacheKey)) { activeRecallCache.delete(cacheKey); } activeRecallCache.set(cacheKey, { - expiresAt: now + ttlMs, + expiresAt, result, }); while (activeRecallCache.size > DEFAULT_MAX_CACHE_ENTRIES) { @@ -1392,9 +1409,13 @@ function setCachedResult(cacheKey: string, result: ActiveRecallResult, ttlMs: nu } } -function sweepExpiredCacheEntries(now = Date.now()): void { +function sweepExpiredCacheEntries(now = asDateTimestampMs(Date.now())): void { + if (now === undefined) { + activeRecallCache.clear(); + return; + } for (const [cacheKey, cached] of activeRecallCache.entries()) { - if (cached.expiresAt <= now) { + if (asDateTimestampMs(cached.expiresAt) === undefined || cached.expiresAt <= now) { activeRecallCache.delete(cacheKey); } }