fix(active-memory): bound recall cache clocks

This commit is contained in:
Peter Steinberger
2026-05-30 11:12:57 -04:00
parent 5adc681238
commit d649548a7a
2 changed files with 73 additions and 8 deletions

View File

@@ -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);

View File

@@ -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);
}
}