From 01bd2f2ecc2a2009f2e68c49e3793fd2e0d0b855 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 07:31:42 +0100 Subject: [PATCH] perf(plugins): reuse active web provider registry --- CHANGELOG.md | 1 + .../web-provider-runtime-shared.test.ts | 75 +++++++++++++++++++ src/plugins/web-provider-runtime-shared.ts | 50 ++++++++++--- 3 files changed, 114 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cae76491b0..e1166b3aa79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Active Memory: use the configured recall timeout as the blocking prompt-build hook budget by default and move cold-start setup grace behind explicit `setupGraceTimeoutMs` config, so the plugin no longer silently extends 15000 ms configs to 45000 ms on the main lane. Fixes #75843. Thanks @vishutdhar. +- Plugins/web-provider: reuse the active gateway plugin registry for runtime web provider resolution after deriving the same candidate plugin ids as the loader path, avoiding a redundant `loadOpenClawPlugins` call on every request while preserving origin and scope filters. Fixes #75513. Thanks @jochen. - Agents/sandbox: preserve existing workspace file modes when sandbox edits atomically replace files, so 0644 files do not collapse to 0600 after Write/Edit/apply_patch. Fixes #44077. Thanks @patosullivan. - Agents/models: keep legacy CLI runtime model refs such as `claude-cli/*` in the configured allowlist after canonical runtime migration, so cron `payload.model` overrides keep working. Fixes #75753. Thanks @RyanSandoval. - Codex/app-server: restart the shared Codex app-server client once when it closes during startup thread resume, preserving the existing thread binding instead of retrying `thread/start` on a closed client. Thanks @vincentkoc. diff --git a/src/plugins/web-provider-runtime-shared.test.ts b/src/plugins/web-provider-runtime-shared.test.ts index b3ffc160cc4..7225ddbde80 100644 --- a/src/plugins/web-provider-runtime-shared.test.ts +++ b/src/plugins/web-provider-runtime-shared.test.ts @@ -6,6 +6,7 @@ const mocks = vi.hoisted(() => ({ resolveCompatibleRuntimePluginRegistry: vi.fn(), resolvePluginRegistryLoadCacheKey: vi.fn((options: unknown) => JSON.stringify(options)), resolveRuntimePluginRegistry: vi.fn(), + getActivePluginRegistry: vi.fn(() => null), getActivePluginRegistryWorkspaceDir: vi.fn(() => undefined), buildPluginRuntimeLoadOptionsFromValues: vi.fn( (_values: unknown, overrides?: Record) => ({ @@ -29,6 +30,7 @@ vi.mock("./loader.js", () => ({ })); vi.mock("./runtime.js", () => ({ + getActivePluginRegistry: mocks.getActivePluginRegistry, getActivePluginRegistryWorkspaceDir: mocks.getActivePluginRegistryWorkspaceDir, })); @@ -56,6 +58,8 @@ describe("web-provider-runtime-shared", () => { JSON.stringify(options), ); mocks.resolveRuntimePluginRegistry.mockReset(); + mocks.getActivePluginRegistry.mockReset(); + mocks.getActivePluginRegistry.mockReturnValue(null); mocks.getActivePluginRegistryWorkspaceDir.mockReset(); mocks.getActivePluginRegistryWorkspaceDir.mockReturnValue(undefined); mocks.buildPluginRuntimeLoadOptionsFromValues.mockReset(); @@ -156,4 +160,75 @@ describe("web-provider-runtime-shared", () => { }), ); }); + + it("reuses the active registry after deriving web provider candidates from resolved config", () => { + const activeRegistry = { source: "active" }; + const resolvedConfig = { plugins: { entries: { brave: { enabled: true } } } }; + const resolveCandidatePluginIds = vi.fn(() => ["brave"]); + const mapRegistryProviders = vi.fn(() => ["provider"]); + mocks.resolveCompatibleRuntimePluginRegistry.mockReturnValue(null); + mocks.getActivePluginRegistry.mockReturnValue(activeRegistry); + + const providers = resolvePluginWebProviders( + { + config: { plugins: { entries: {} } }, + env: { BRAVE_API_KEY: "key" }, + onlyPluginIds: ["brave", "firecrawl"], + origin: "bundled", + workspaceDir: "/workspace", + }, + { + resolveBundledResolutionConfig: () => ({ + config: resolvedConfig, + activationSourceConfig: { plugins: { entries: {} } }, + autoEnabledReasons: { brave: ["env"] }, + }), + resolveCandidatePluginIds, + mapRegistryProviders, + }, + ); + + expect(providers).toEqual(["provider"]); + expect(resolveCandidatePluginIds).toHaveBeenCalledWith({ + config: resolvedConfig, + workspaceDir: "/workspace", + env: { BRAVE_API_KEY: "key" }, + onlyPluginIds: ["brave", "firecrawl"], + origin: "bundled", + }); + expect(mapRegistryProviders).toHaveBeenCalledWith({ + registry: activeRegistry, + onlyPluginIds: ["brave"], + }); + expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled(); + }); + + it("preserves explicit empty candidate scopes when reusing the active registry", () => { + const activeRegistry = { source: "active" }; + const mapRegistryProviders = vi.fn(() => []); + mocks.resolveCompatibleRuntimePluginRegistry.mockReturnValue(null); + mocks.getActivePluginRegistry.mockReturnValue(activeRegistry); + + resolvePluginWebProviders( + { + config: {}, + onlyPluginIds: [], + }, + { + resolveBundledResolutionConfig: () => ({ + config: {}, + activationSourceConfig: {}, + autoEnabledReasons: {}, + }), + resolveCandidatePluginIds: () => [], + mapRegistryProviders, + }, + ); + + expect(mapRegistryProviders).toHaveBeenCalledWith({ + registry: activeRegistry, + onlyPluginIds: [], + }); + expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled(); + }); }); diff --git a/src/plugins/web-provider-runtime-shared.ts b/src/plugins/web-provider-runtime-shared.ts index d19f72bc7d8..33194170b3e 100644 --- a/src/plugins/web-provider-runtime-shared.ts +++ b/src/plugins/web-provider-runtime-shared.ts @@ -9,7 +9,7 @@ import type { PluginLoadOptions } from "./loader.js"; import type { PluginManifestRecord } from "./manifest-registry.js"; import { hasExplicitPluginIdScope, normalizePluginIdScope } from "./plugin-scope.js"; import type { PluginRegistry } from "./registry.js"; -import { getActivePluginRegistryWorkspaceDir } from "./runtime.js"; +import { getActivePluginRegistry, getActivePluginRegistryWorkspaceDir } from "./runtime.js"; import { buildPluginRuntimeLoadOptionsFromValues, createPluginRuntimeLoaderLogger, @@ -58,7 +58,7 @@ type ResolveWebProviderRuntimeDeps = { }) => TEntry[] | null; }; -function resolveWebProviderLoadOptions( +function resolveWebProviderRuntimeContext( params: ResolvePluginWebProvidersParams, deps: ResolveWebProviderRuntimeDeps, ) { @@ -79,19 +79,35 @@ function resolveWebProviderLoadOptions( origin: params.origin, }), ); + return { + activationSourceConfig, + autoEnabledReasons, + config, + env, + onlyPluginIds, + workspaceDir, + }; +} + +function resolveWebProviderLoadOptions( + context: ReturnType>, + params: ResolvePluginWebProvidersParams, +) { return buildPluginRuntimeLoadOptionsFromValues( { - env, - config, - activationSourceConfig, - autoEnabledReasons, - workspaceDir, + env: context.env, + config: context.config, + activationSourceConfig: context.activationSourceConfig, + autoEnabledReasons: context.autoEnabledReasons, + workspaceDir: context.workspaceDir, logger: createPluginRuntimeLoaderLogger(), }, { cache: params.cache ?? false, activate: params.activate ?? false, - ...(hasExplicitPluginIdScope(onlyPluginIds) ? { onlyPluginIds } : {}), + ...(hasExplicitPluginIdScope(context.onlyPluginIds) + ? { onlyPluginIds: context.onlyPluginIds } + : {}), }, ); } @@ -149,20 +165,28 @@ export function resolvePluginWebProviders( return deps.mapRegistryProviders({ registry, onlyPluginIds: pluginIds }); } - const loadOptions = resolveWebProviderLoadOptions(params, deps); + const context = resolveWebProviderRuntimeContext(params, deps); + const loadOptions = resolveWebProviderLoadOptions(context, params); const compatible = resolveCompatibleRuntimePluginRegistry(loadOptions); if (compatible) { return deps.mapRegistryProviders({ registry: compatible, - onlyPluginIds: params.onlyPluginIds, + onlyPluginIds: context.onlyPluginIds, }); } if (isPluginRegistryLoadInFlight(loadOptions)) { return []; } + const activeRegistry = getActivePluginRegistry(); + if (activeRegistry) { + return deps.mapRegistryProviders({ + registry: activeRegistry, + onlyPluginIds: context.onlyPluginIds, + }); + } return deps.mapRegistryProviders({ registry: loadOpenClawPlugins(loadOptions), - onlyPluginIds: params.onlyPluginIds, + onlyPluginIds: context.onlyPluginIds, }); } @@ -171,7 +195,9 @@ export function resolveRuntimeWebProviders( deps: ResolveWebProviderRuntimeDeps, ): TEntry[] { const loadOptions = - params.config === undefined ? undefined : resolveWebProviderLoadOptions(params, deps); + params.config === undefined + ? undefined + : resolveWebProviderLoadOptions(resolveWebProviderRuntimeContext(params, deps), params); const runtimeRegistry = resolveRuntimePluginRegistry(loadOptions); if (runtimeRegistry) { return deps.mapRegistryProviders({