diff --git a/CHANGELOG.md b/CHANGELOG.md index 088be466158..947b1b155d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai - CLI/config: keep JSON dry-run patches validating touched channel configuration against bundled channel schemas even when the patch only contains SecretRef objects. - Plugins/tools: keep disabled bundled tool plugins out of explicit runtime allowlist ownership and fall back from loaded-but-empty channel registries to tool-bearing plugin registries, so Active Memory can use bundled `memory-core` search/get tools even when `memory-lancedb` is disabled. Fixes #76603. Thanks @jwong-art. - Plugins/install: run `npm install` from the managed npm-root manifest so installing one `@openclaw/*` plugin preserves already installed sibling plugins instead of pruning them. Fixes #76571. (#76602) Thanks @byungskers and @crpol. +- Plugins/tools: restore on-demand registry load for path-based plugins (origin "config") so tool factories registered via `plugins.load.paths` are resolved at agent request time when no pre-warmed channel registry is present; prevents "unknown method" errors after gateway startup. Fixes #76598. Thanks @hclsys. - Channels/QQ Bot: resolve structured `clientSecret` SecretRefs before QQ token exchange, expose the QQ Bot secret contract to secrets tooling, and reject legacy `secretref:/...` marker strings. (#74772) Thanks @xialonglee. - Agents: keep active streamed provider replies alive by refreshing guarded fetch timeouts on raw body chunks and surface true prompt stream timeouts as explicit errors instead of partial assistant fragments. Fixes #76307. (#76633) Thanks @MkDev11. - Plugins/externalization: keep official ACPX, Google Chat, and LINE install specs on production package names, leaving beta-tag probing to the explicit OpenClaw beta update channel. Thanks @vincentkoc. diff --git a/src/plugins/tools.optional.test.ts b/src/plugins/tools.optional.test.ts index ae9a4f8bfe2..565d0578108 100644 --- a/src/plugins/tools.optional.test.ts +++ b/src/plugins/tools.optional.test.ts @@ -491,6 +491,43 @@ describe("resolvePluginTools optional tools", () => { ); }); + it("auto-loads cold registry for path-based (bundled-origin) plugins without pre-warming (#76598)", () => { + const config = createContext().config; + const registry = createToolRegistry([createOptionalDemoEntry()]); + loadOpenClawPluginsMock.mockReturnValue(registry); + installToolManifestSnapshot({ + config, + plugin: { + id: "optional-demo", + origin: "bundled", + enabledByDefault: true, + channels: [], + providers: [], + contracts: { + tools: ["optional_tool"], + }, + }, + }); + + // No ensureStandalonePluginToolRegistryLoaded pre-call and no pinned channel registry — + // resolvePluginTools must trigger standalone load itself when the registry is cold. + // This is the regression path from PR #76004 where path-based plugin tools disappeared. + const tools = resolvePluginTools( + createResolveToolsParams({ + toolAllowlist: ["optional_tool"], + }), + ); + + expectResolvedToolNames(tools, ["optional_tool"]); + expect(loadOpenClawPluginsMock).toHaveBeenCalledWith( + expect.objectContaining({ + activate: false, + onlyPluginIds: ["optional-demo"], + toolDiscovery: true, + }), + ); + }); + it("does not reuse a pinned gateway registry for manifest-unavailable tools", () => { const config = createContext().config; installToolManifestSnapshot({ diff --git a/src/plugins/tools.ts b/src/plugins/tools.ts index 8a25bd8bab0..50b56913997 100644 --- a/src/plugins/tools.ts +++ b/src/plugins/tools.ts @@ -804,12 +804,26 @@ export function resolvePluginTools(params: { onlyPluginIds: runtimePluginIds, runtimeOptions, }); - const registry = resolvePluginToolRegistry({ + let registry = resolvePluginToolRegistry({ loadOptions, onlyPluginIds: runtimePluginIds, }); if (!registry) { - return tools; + // Cold registry: path-based plugins (origin "config") registered via plugins.load.paths + // are not pinned to any active channel/surface registry until explicitly loaded. + // Trigger a standalone load so their tool factories become available, then retry. + ensureStandaloneRuntimePluginRegistryLoaded({ + surface: "channel", + requiredPluginIds: runtimePluginIds, + loadOptions, + }); + registry = resolvePluginToolRegistry({ + loadOptions, + onlyPluginIds: runtimePluginIds, + }); + if (!registry) { + return tools; + } } const scopedPluginIds = new Set(runtimePluginIds);