From beff8743406a71c93fc9aaecee9a9e1d51a4ea36 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 15:03:38 +0100 Subject: [PATCH] perf(test): trim active memory and qa lab hotspots --- extensions/active-memory/index.test.ts | 82 +++++++++++++----------- extensions/active-memory/index.ts | 31 ++++++++- extensions/qa-lab/src/lab-server.test.ts | 9 ++- 3 files changed, 75 insertions(+), 47 deletions(-) diff --git a/extensions/active-memory/index.test.ts b/extensions/active-memory/index.test.ts index 9cfa3bd1b9c..82931984578 100644 --- a/extensions/active-memory/index.test.ts +++ b/extensions/active-memory/index.test.ts @@ -3,7 +3,7 @@ import os from "node:os"; import path from "node:path"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; -import plugin from "./index.js"; +import plugin, { __testing } from "./index.js"; function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -119,10 +119,12 @@ describe("active-memory plugin", () => { runEmbeddedPiAgent.mockResolvedValue({ payloads: [{ text: "- lemon pepper wings\n- blue cheese" }], }); + __testing.resetActiveRecallCacheForTests(); plugin.register(api as unknown as OpenClawPluginApi); }); afterEach(async () => { + vi.useRealTimers(); vi.restoreAllMocks(); if (stateDir) { await fs.rm(stateDir, { recursive: true, force: true }); @@ -1022,9 +1024,10 @@ describe("active-memory plugin", () => { }); it("does not cache timeout results", async () => { + __testing.setMinimumTimeoutMsForTests(1); api.pluginConfig = { agents: ["main"], - timeoutMs: 250, + timeoutMs: 1, logging: true, }; plugin.register(api as unknown as OpenClawPluginApi); @@ -1032,12 +1035,15 @@ describe("active-memory plugin", () => { runEmbeddedPiAgent.mockImplementation(async (params: { abortSignal?: AbortSignal }) => { lastAbortSignal = params.abortSignal; return await new Promise((resolve, reject) => { - const abortHandler = () => reject(new Error("aborted")); - params.abortSignal?.addEventListener("abort", abortHandler, { once: true }); - setTimeout(() => { + const timer = setTimeout(() => { params.abortSignal?.removeEventListener("abort", abortHandler); resolve({ payloads: [] }); }, 2_000); + const abortHandler = () => { + clearTimeout(timer); + reject(new Error("aborted")); + }; + params.abortSignal?.addEventListener("abort", abortHandler, { once: true }); }); }); @@ -1102,14 +1108,15 @@ describe("active-memory plugin", () => { }); it("ignores late subagent payloads once the active-memory timeout signal has fired", async () => { + __testing.setMinimumTimeoutMsForTests(1); api.pluginConfig = { agents: ["main"], - timeoutMs: 250, + timeoutMs: 1, logging: true, }; plugin.register(api as unknown as OpenClawPluginApi); runEmbeddedPiAgent.mockImplementationOnce(async (params: { timeoutMs?: number }) => { - await new Promise((resolve) => setTimeout(resolve, (params.timeoutMs ?? 0) + 25)); + await new Promise((resolve) => setTimeout(resolve, (params.timeoutMs ?? 0) + 1)); return { payloads: [{ text: "late timeout payload that should never become memory context" }], meta: { aborted: true }, @@ -1944,45 +1951,42 @@ describe("active-memory plugin", () => { expect(lines.some((line) => line.includes("\r"))).toBe(false); }); - it("caps the active-memory cache size and evicts the oldest entries", async () => { - api.pluginConfig = { - agents: ["main"], - logging: true, - }; - plugin.register(api as unknown as OpenClawPluginApi); - + it("caps the active-memory cache size and evicts the oldest entries", () => { + const sessionKey = "agent:main:cache-cap"; for (let index = 0; index <= 1000; index += 1) { - await hooks.before_prompt_build( - { prompt: `cache pressure prompt ${index}`, messages: [] }, - { + __testing.setCachedResult( + __testing.buildCacheKey({ agentId: "main", - trigger: "user", - sessionKey: "agent:main:cache-cap", - messageProvider: "webchat", + sessionKey, + query: `cache pressure prompt ${index}`, + }), + { + status: "ok", + elapsedMs: 1, + rawReply: `memory ${index}`, + summary: `memory ${index}`, }, + 15_000, ); } - const callsBeforeReplay = runEmbeddedPiAgent.mock.calls.length; - - await hooks.before_prompt_build( - { prompt: "cache pressure prompt 0", messages: [] }, - { - agentId: "main", - trigger: "user", - sessionKey: "agent:main:cache-cap", - messageProvider: "webchat", - }, - ); - - expect(runEmbeddedPiAgent.mock.calls.length).toBe(callsBeforeReplay + 1); - const infoLines = vi - .mocked(api.logger.info) - .mock.calls.map((call: unknown[]) => String(call[0])); expect( - infoLines.some( - (line: string) => line.includes("cached status=ok") && line.includes("prompt 0"), + __testing.getCachedResult( + __testing.buildCacheKey({ + agentId: "main", + sessionKey, + query: "cache pressure prompt 0", + }), ), - ).toBe(false); + ).toBeUndefined(); + expect( + __testing.getCachedResult( + __testing.buildCacheKey({ + agentId: "main", + sessionKey, + query: "cache pressure prompt 1", + }), + ), + ).toMatchObject({ status: "ok", summary: "memory 1" }); }); }); diff --git a/extensions/active-memory/index.ts b/extensions/active-memory/index.ts index c30e11ad19f..c3e2290b602 100644 --- a/extensions/active-memory/index.ts +++ b/extensions/active-memory/index.ts @@ -25,6 +25,8 @@ const DEFAULT_RECENT_USER_CHARS = 220; const DEFAULT_RECENT_ASSISTANT_CHARS = 180; const DEFAULT_CACHE_TTL_MS = 15_000; const DEFAULT_MAX_CACHE_ENTRIES = 1000; +const CACHE_SWEEP_INTERVAL_MS = 1000; +const DEFAULT_MIN_TIMEOUT_MS = 250; const DEFAULT_QUERY_MODE = "recent" as const; const DEFAULT_QMD_SEARCH_MODE = "search" as const; const DEFAULT_TRANSCRIPT_DIR = "active-memory"; @@ -173,6 +175,8 @@ type ActiveMemoryToggleStore = { type AsyncLock = (task: () => Promise) => Promise; const toggleStoreLocks = new Map(); +let lastActiveRecallCacheSweepAt = 0; +let minimumTimeoutMs = DEFAULT_MIN_TIMEOUT_MS; function createAsyncLock(): AsyncLock { let lock: Promise = Promise.resolve(); @@ -632,7 +636,7 @@ function normalizePluginConfig(pluginConfig: unknown): ResolvedActiveRecallPlugi timeoutMs: clampInt( parseOptionalPositiveInt(raw.timeoutMs, DEFAULT_TIMEOUT_MS), DEFAULT_TIMEOUT_MS, - 250, + minimumTimeoutMs, 120_000, ), queryMode: @@ -944,12 +948,19 @@ function getCachedResult(cacheKey: string): ActiveRecallResult | undefined { } function setCachedResult(cacheKey: string, result: ActiveRecallResult, ttlMs: number): void { - sweepExpiredCacheEntries(); + const now = Date.now(); + if ( + activeRecallCache.size >= DEFAULT_MAX_CACHE_ENTRIES || + now - lastActiveRecallCacheSweepAt >= CACHE_SWEEP_INTERVAL_MS + ) { + sweepExpiredCacheEntries(now); + lastActiveRecallCacheSweepAt = now; + } if (activeRecallCache.has(cacheKey)) { activeRecallCache.delete(cacheKey); } activeRecallCache.set(cacheKey, { - expiresAt: Date.now() + ttlMs, + expiresAt: now + ttlMs, result, }); while (activeRecallCache.size > DEFAULT_MAX_CACHE_ENTRIES) { @@ -2027,3 +2038,17 @@ export default definePluginEntry({ }); }, }); + +export const __testing = { + buildCacheKey, + getCachedResult, + resetActiveRecallCacheForTests() { + activeRecallCache.clear(); + lastActiveRecallCacheSweepAt = 0; + minimumTimeoutMs = DEFAULT_MIN_TIMEOUT_MS; + }, + setMinimumTimeoutMsForTests(value: number) { + minimumTimeoutMs = value; + }, + setCachedResult, +}; diff --git a/extensions/qa-lab/src/lab-server.test.ts b/extensions/qa-lab/src/lab-server.test.ts index bc17f0c8e14..3487207dde1 100644 --- a/extensions/qa-lab/src/lab-server.test.ts +++ b/extensions/qa-lab/src/lab-server.test.ts @@ -42,7 +42,7 @@ async function fetchWithRetry(input: string, init?: RequestInit, attempts = 3) { if (attempt === attempts) { throw error; } - await sleep(50); + await sleep(10); } } throw lastError; @@ -61,7 +61,7 @@ async function waitForRunnerCatalog(baseUrl: string, timeoutMs = 5_000) { if (bootstrap.runnerCatalog.status !== "loading") { return bootstrap.runnerCatalog; } - await sleep(50); + await sleep(10); } throw new Error("runner catalog stayed loading"); } @@ -75,7 +75,7 @@ async function waitForFile(filePath: string, timeoutMs = 5_000) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") { throw error; } - await sleep(50); + await sleep(10); } } throw new Error(`file did not appear: ${filePath}`); @@ -417,7 +417,7 @@ describe("qa-lab server", () => { await lab.stop(); }); - await sleep(150); + await sleep(25); await expect(readFile(markerPath, "utf8")).rejects.toThrow(); const bootstrapResponse = await fetchWithRetry(`${lab.baseUrl}/api/bootstrap`); @@ -501,7 +501,6 @@ describe("qa-lab server", () => { }), }); - await new Promise((resolve) => setTimeout(resolve, 800)); const snapshot = (await (await fetchWithRetry(`${lab.baseUrl}/api/state`)).json()) as { messages: Array<{ direction: string }>; };