fix(plugins): cache web provider runtime loads

This commit is contained in:
Peter Steinberger
2026-05-02 08:32:57 +01:00
parent 2c14d6f99d
commit d13a2063c4
3 changed files with 138 additions and 6 deletions

View File

@@ -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.

View File

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

View File

@@ -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<TEntry>(
},
{
onlyPluginIds: pluginIds,
cache: params.cache ?? false,
cache: params.cache ?? true,
activate: params.activate ?? false,
},
),
@@ -186,16 +186,21 @@ export function resolvePluginWebProviders<TEntry>(
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),
});