From d13a2063c4216aadee19a8cf480d3037e0d78331 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 08:32:57 +0100 Subject: [PATCH] fix(plugins): cache web provider runtime loads --- CHANGELOG.md | 5 +- .../web-provider-runtime-shared.test.ts | 128 ++++++++++++++++++ src/plugins/web-provider-runtime-shared.ts | 11 +- 3 files changed, 138 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index caf1eb0cd45..52b0b2d42f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,9 +44,8 @@ Docs: https://docs.openclaw.ai - Web search/Brave: add `plugins.entries.brave.config.webSearch.baseUrl` for Brave-compatible proxies, including endpoint-aware cache keys for both web and LLM Context modes. Fixes #19075. Thanks @jkoprax and @vishnukool. - Web search/config: validate explicit `tools.web.search.provider` values against bundled and installed plugin manifests, while warning for stale third-party plugin config. Fixes #53092. Thanks @TinyTb. - Web search/SearXNG: retry empty non-general category searches once with the general category, so unsupported category engines do not return empty results when general search has matches. Fixes #73552. Thanks @Loukky. -- CLI/message: skip gateway-stop hooks for read-only `message read` and bound - stop-hook shutdown for other message actions, so one-shot Discord reads cannot - hang behind plugin lifecycle cleanup. +- CLI/message: skip gateway-stop hooks for read-only `message read` and bound stop-hook shutdown for other message actions, so one-shot Discord reads cannot hang behind plugin lifecycle cleanup. +- Plugins/web-provider: cache repeated bundled web search and web fetch provider registry loads by default while preserving explicit cache opt-outs. Supersedes #75992. Thanks @DmitryPogodaev. - 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 a1203817772..178d8c78028 100644 --- a/src/plugins/web-provider-runtime-shared.test.ts +++ b/src/plugins/web-provider-runtime-shared.test.ts @@ -231,4 +231,132 @@ describe("web-provider-runtime-shared", () => { }); expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled(); }); + + it("caches runtime web provider plugin loads by default", () => { + const loadedRegistry = { source: "loaded" }; + const mapRegistryProviders = vi.fn(() => ["provider"]); + mocks.loadOpenClawPlugins.mockReturnValue(loadedRegistry as never); + + const providers = resolvePluginWebProviders( + { + config: {}, + onlyPluginIds: ["brave"], + }, + { + resolveBundledResolutionConfig: () => ({ + config: {}, + activationSourceConfig: {}, + autoEnabledReasons: {}, + }), + resolveCandidatePluginIds: () => ["brave"], + mapRegistryProviders, + }, + ); + + expect(providers).toEqual(["provider"]); + expect(mocks.resolveCompatibleRuntimePluginRegistry).toHaveBeenCalledWith( + expect.objectContaining({ + cache: true, + onlyPluginIds: ["brave"], + }), + ); + expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith( + expect.objectContaining({ + cache: true, + onlyPluginIds: ["brave"], + }), + ); + }); + + it("keeps explicit runtime web provider cache opt-outs", () => { + const loadedRegistry = { source: "loaded" }; + const mapRegistryProviders = vi.fn(() => ["provider"]); + mocks.loadOpenClawPlugins.mockReturnValue(loadedRegistry as never); + + resolvePluginWebProviders( + { + cache: false, + config: {}, + onlyPluginIds: ["brave"], + }, + { + resolveBundledResolutionConfig: () => ({ + config: {}, + activationSourceConfig: {}, + autoEnabledReasons: {}, + }), + resolveCandidatePluginIds: () => ["brave"], + mapRegistryProviders, + }, + ); + + expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith( + expect.objectContaining({ + cache: false, + onlyPluginIds: ["brave"], + }), + ); + }); + + it("caches setup web provider plugin loads by default", () => { + const loadedRegistry = { source: "setup" }; + const mapRegistryProviders = vi.fn(() => ["provider"]); + mocks.loadOpenClawPlugins.mockReturnValue(loadedRegistry as never); + + const providers = resolvePluginWebProviders( + { + config: {}, + mode: "setup", + }, + { + resolveBundledResolutionConfig: () => ({ + config: {}, + activationSourceConfig: {}, + autoEnabledReasons: {}, + }), + resolveCandidatePluginIds: () => ["brave"], + mapRegistryProviders, + resolveBundledPublicArtifactProviders: () => null, + }, + ); + + expect(providers).toEqual(["provider"]); + expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith( + expect.objectContaining({ + cache: true, + onlyPluginIds: ["brave"], + }), + ); + }); + + it("keeps explicit setup web provider cache opt-outs", () => { + const loadedRegistry = { source: "setup" }; + const mapRegistryProviders = vi.fn(() => ["provider"]); + mocks.loadOpenClawPlugins.mockReturnValue(loadedRegistry as never); + + resolvePluginWebProviders( + { + cache: false, + config: {}, + mode: "setup", + }, + { + resolveBundledResolutionConfig: () => ({ + config: {}, + activationSourceConfig: {}, + autoEnabledReasons: {}, + }), + resolveCandidatePluginIds: () => ["brave"], + mapRegistryProviders, + resolveBundledPublicArtifactProviders: () => null, + }, + ); + + expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith( + expect.objectContaining({ + cache: false, + onlyPluginIds: ["brave"], + }), + ); + }); }); diff --git a/src/plugins/web-provider-runtime-shared.ts b/src/plugins/web-provider-runtime-shared.ts index 8b5f34865e0..6ba2e1e746b 100644 --- a/src/plugins/web-provider-runtime-shared.ts +++ b/src/plugins/web-provider-runtime-shared.ts @@ -112,7 +112,7 @@ function resolveWebProviderLoadOptions( logger: createPluginRuntimeLoaderLogger(), }, { - cache: params.cache ?? false, + cache: params.cache ?? true, activate: params.activate ?? false, ...(hasExplicitPluginIdScope(context.onlyPluginIds) ? { onlyPluginIds: context.onlyPluginIds } @@ -166,7 +166,7 @@ export function resolvePluginWebProviders( }, { onlyPluginIds: pluginIds, - cache: params.cache ?? false, + cache: params.cache ?? true, activate: params.activate ?? false, }, ), @@ -186,16 +186,21 @@ export function resolvePluginWebProviders( if (isPluginRegistryLoadInFlight(loadOptions)) { return []; } + const scopedPluginIds = context.onlyPluginIds; + const hasExplicitEmptyScope = scopedPluginIds !== undefined && scopedPluginIds.length === 0; const activeRegistry = getActivePluginRegistry(); if (activeRegistry) { const activeProviders = deps.mapRegistryProviders({ registry: activeRegistry, onlyPluginIds: context.onlyPluginIds, }); - if (activeProviders.length > 0) { + if (activeProviders.length > 0 || hasExplicitEmptyScope) { return activeProviders; } } + if (hasExplicitEmptyScope) { + return []; + } return deps.mapRegistryProviders({ registry: loadOpenClawPlugins(loadOptions), });