From 2cc777539a6f06b30836a15aa6ed6b2dcedda85d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Mar 2026 02:03:23 +0000 Subject: [PATCH] perf: reduce plugin and memory startup overhead --- src/memory/manager-sync-ops.ts | 14 +++-- .../manager.sync-errors-do-not-crash.test.ts | 60 +++---------------- src/plugins/sdk-alias.ts | 35 ++++++++--- 3 files changed, 42 insertions(+), 67 deletions(-) diff --git a/src/memory/manager-sync-ops.ts b/src/memory/manager-sync-ops.ts index 21aa4893147..0822dd41978 100644 --- a/src/memory/manager-sync-ops.ts +++ b/src/memory/manager-sync-ops.ts @@ -94,6 +94,12 @@ function shouldIgnoreMemoryWatchPath(watchPath: string): boolean { return parts.some((segment) => IGNORED_MEMORY_WATCH_DIR_NAMES.has(segment)); } +export function runDetachedMemorySync(sync: () => Promise, reason: "interval" | "watch") { + void sync().catch((err) => { + log.warn(`memory sync failed (${reason}): ${String(err)}`); + }); +} + export abstract class MemoryManagerSyncOps { protected abstract readonly cfg: OpenClawConfig; protected abstract readonly agentId: string; @@ -650,9 +656,7 @@ export abstract class MemoryManagerSyncOps { } const ms = minutes * 60 * 1000; this.intervalTimer = setInterval(() => { - void this.sync({ reason: "interval" }).catch((err) => { - log.warn(`memory sync failed (interval): ${String(err)}`); - }); + runDetachedMemorySync(() => this.sync({ reason: "interval" }), "interval"); }, ms); } @@ -665,9 +669,7 @@ export abstract class MemoryManagerSyncOps { } this.watchTimer = setTimeout(() => { this.watchTimer = null; - void this.sync({ reason: "watch" }).catch((err) => { - log.warn(`memory sync failed (watch): ${String(err)}`); - }); + runDetachedMemorySync(() => this.sync({ reason: "watch" }), "watch"); }, this.settings.sync.watchDebounceMs); } diff --git a/src/memory/manager.sync-errors-do-not-crash.test.ts b/src/memory/manager.sync-errors-do-not-crash.test.ts index dd9a820d1ec..f649eab5afd 100644 --- a/src/memory/manager.sync-errors-do-not-crash.test.ts +++ b/src/memory/manager.sync-errors-do-not-crash.test.ts @@ -1,37 +1,13 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { OpenClawConfig } from "../config/config.js"; -import { getEmbedBatchMock, resetEmbeddingMocks } from "./embedding.test-mocks.js"; -import type { MemoryIndexManager } from "./index.js"; -import { getRequiredMemoryIndexManager } from "./test-manager-helpers.js"; +import { runDetachedMemorySync } from "./manager-sync-ops.js"; describe("memory manager sync failures", () => { - let workspaceDir: string; - let indexPath: string; - let manager: MemoryIndexManager | null = null; - const embedBatch = getEmbedBatchMock(); - - beforeEach(async () => { + beforeEach(() => { vi.useFakeTimers(); - resetEmbeddingMocks(); - embedBatch.mockImplementation(async () => { - throw new Error("openai embeddings failed: 400 bad request"); - }); - workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-")); - indexPath = path.join(workspaceDir, "index.sqlite"); - await fs.mkdir(path.join(workspaceDir, "memory")); - await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), "Hello"); }); afterEach(async () => { vi.useRealTimers(); - if (manager) { - await manager.close(); - manager = null; - } - await fs.rm(workspaceDir, { recursive: true, force: true }); }); it("does not raise unhandledRejection when watch-triggered sync fails", async () => { @@ -40,38 +16,16 @@ describe("memory manager sync failures", () => { unhandled.push(reason); }; process.on("unhandledRejection", handler); - - const cfg = { - agents: { - defaults: { - workspace: workspaceDir, - memorySearch: { - provider: "openai", - model: "mock-embed", - store: { path: indexPath, vector: { enabled: false } }, - cache: { enabled: false }, - query: { minScore: 0, hybrid: { enabled: false } }, - sync: { watch: true, watchDebounceMs: 1, onSessionStart: false, onSearch: false }, - }, - }, - list: [{ id: "main", default: true }], - }, - } as OpenClawConfig; - - manager = await getRequiredMemoryIndexManager({ cfg, agentId: "main", purpose: "status" }); const syncSpy = vi - .spyOn(manager, "sync") + .fn() .mockRejectedValueOnce(new Error("openai embeddings failed: 400 bad request")); - - // Call the internal scheduler directly; it uses fire-and-forget sync. - (manager as unknown as { scheduleWatchSync: () => void }).scheduleWatchSync(); + setTimeout(() => { + runDetachedMemorySync(syncSpy, "watch"); + }, 1); await vi.runOnlyPendingTimersAsync(); - const syncPromise = syncSpy.mock.results[0]?.value as Promise | undefined; vi.useRealTimers(); - if (syncPromise) { - await syncPromise.catch(() => undefined); - } + await syncSpy.mock.results[0]?.value?.catch(() => undefined); process.off("unhandledRejection", handler); expect(unhandled).toHaveLength(0); diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts index b1a59917f1c..b8801dae6ed 100644 --- a/src/plugins/sdk-alias.ts +++ b/src/plugins/sdk-alias.ts @@ -205,6 +205,7 @@ export function resolvePluginSdkAliasFile(params: { } const cachedPluginSdkExportedSubpaths = new Map(); +const cachedPluginSdkScopedAliasMaps = new Map>(); export function listPluginSdkExportedSubpaths(params: { modulePath?: string } = {}): string[] { const modulePath = params.modulePath ?? fileURLToPath(import.meta.url); @@ -224,17 +225,35 @@ export function listPluginSdkExportedSubpaths(params: { modulePath?: string } = export function resolvePluginSdkScopedAliasMap( params: { modulePath?: string } = {}, ): Record { + const modulePath = params.modulePath ?? fileURLToPath(import.meta.url); + const packageRoot = resolveLoaderPluginSdkPackageRoot({ modulePath }); + if (!packageRoot) { + return {}; + } + const orderedKinds = resolvePluginSdkAliasCandidateOrder({ + modulePath, + isProduction: process.env.NODE_ENV === "production", + }); + const cacheKey = `${packageRoot}::${orderedKinds.join(",")}`; + const cached = cachedPluginSdkScopedAliasMaps.get(cacheKey); + if (cached) { + return cached; + } const aliasMap: Record = {}; - for (const subpath of listPluginSdkExportedSubpaths(params)) { - const resolved = resolvePluginSdkAliasFile({ - srcFile: `${subpath}.ts`, - distFile: `${subpath}.js`, - modulePath: params.modulePath, - }); - if (resolved) { - aliasMap[`openclaw/plugin-sdk/${subpath}`] = resolved; + for (const subpath of listPluginSdkExportedSubpaths({ modulePath })) { + const candidateMap = { + src: path.join(packageRoot, "src", "plugin-sdk", `${subpath}.ts`), + dist: path.join(packageRoot, "dist", "plugin-sdk", `${subpath}.js`), + } as const; + for (const kind of orderedKinds) { + const candidate = candidateMap[kind]; + if (fs.existsSync(candidate)) { + aliasMap[`openclaw/plugin-sdk/${subpath}`] = candidate; + break; + } } } + cachedPluginSdkScopedAliasMaps.set(cacheKey, aliasMap); return aliasMap; }