diff --git a/CHANGELOG.md b/CHANGELOG.md index 14de4ea6b99..535d8ad2098 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Web fetch: scope provider fallback cache entries by the selected fetch provider so config reloads cannot reuse another provider's cached fallback payload. Thanks @vincentkoc. - Web search: honor late-bound `tools.web.search.enabled: false` during tool execution so config reloads cannot leave an already-created `web_search` tool runnable. Thanks @vincentkoc. - Plugins/packages: reject inferred built runtime entries that exist but fail package-boundary checks instead of falling back to TypeScript source for installed packages. Thanks @vincentkoc. - Plugins/loader: do not retry native-loaded JavaScript plugin modules through the source transformer after native evaluation has already reached a missing dependency, avoiding duplicate top-level side effects. Thanks @vincentkoc. diff --git a/src/agents/tools/web-fetch.provider-fallback.test.ts b/src/agents/tools/web-fetch.provider-fallback.test.ts index 80ad644efbb..9777627e65c 100644 --- a/src/agents/tools/web-fetch.provider-fallback.test.ts +++ b/src/agents/tools/web-fetch.provider-fallback.test.ts @@ -221,4 +221,77 @@ describe("web_fetch provider fallback normalization", () => { }), ); }); + + it("scopes provider fallback cache entries by the late-bound provider", async () => { + global.fetch = withFetchPreconnect( + vi.fn(async () => { + throw new Error("network failed"); + }), + ); + resolveWebFetchDefinitionMock.mockImplementation( + ({ runtimeWebFetch }: { runtimeWebFetch?: { selectedProvider?: string } }) => { + const providerId = runtimeWebFetch?.selectedProvider ?? "unknown"; + return { + provider: { id: providerId }, + definition: { + description: providerId, + parameters: {}, + execute: async () => ({ + text: `${providerId} fallback body`, + }), + }, + }; + }, + ); + + const executeWithProvider = async (providerId: string) => { + runtimeState.activeSecretsRuntimeSnapshot = { + config: { + tools: { + web: { + fetch: { + provider: providerId, + }, + }, + }, + }, + }; + runtimeState.activeRuntimeWebToolsMetadata = { + fetch: { + providerConfigured: providerId, + providerSource: "configured", + selectedProvider: providerId, + selectedProviderKeySource: "config", + diagnostics: [], + }, + diagnostics: [], + }; + const tool = createWebFetchTool({ + config: {} as OpenClawConfig, + sandboxed: false, + lateBindRuntimeConfig: true, + }); + return tool?.execute?.("call-provider-fallback", { + url: "https://example.com/provider-cache-scope", + }); + }; + + const first = await executeWithProvider("firecrawl"); + const second = await executeWithProvider("perplexity-fetch"); + const firstDetails = first?.details as { + externalContent?: { provider?: string }; + text?: string; + }; + const secondDetails = second?.details as { + cached?: boolean; + externalContent?: { provider?: string }; + text?: string; + }; + + expect(firstDetails.externalContent?.provider).toBe("firecrawl"); + expect(firstDetails.text).toContain("firecrawl fallback body"); + expect(secondDetails.externalContent?.provider).toBe("perplexity-fetch"); + expect(secondDetails.text).toContain("perplexity-fetch fallback body"); + expect(secondDetails.cached).toBeUndefined(); + }); }); diff --git a/src/agents/tools/web-fetch.ts b/src/agents/tools/web-fetch.ts index 2a9f7419701..917f8e075d1 100644 --- a/src/agents/tools/web-fetch.ts +++ b/src/agents/tools/web-fetch.ts @@ -283,6 +283,7 @@ type WebFetchRuntimeParams = { allowRfc2544BenchmarkRange?: boolean; allowIpv6UniqueLocalRange?: boolean; }; + providerCacheKey?: string; lookupFn?: LookupFn; resolveProviderFallback: () => Promise; }; @@ -407,7 +408,7 @@ async function runWebFetch(params: WebFetchRuntimeParams): Promise