fix(plugins): restore cold-registry load for path-based plugin tools (#76598)

`resolvePluginTools` returned an empty tool list when no pre-warmed
channel/active registry was found after startup — the on-demand fallback
removed by PR #76004 was only added back for memory and capability-provider
surfaces, leaving path-based (origin "config") plugin tool factories silent.

Fix: when `resolvePluginToolRegistry` returns null, trigger a standalone
registry load via `ensureStandaloneRuntimePluginRegistryLoaded`, then retry.
Adds regression test asserting tools are resolved without pre-warming.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HCL
2026-05-03 18:11:04 +08:00
committed by Peter Steinberger
parent 8ebf86cdff
commit a3b94f3910
3 changed files with 54 additions and 2 deletions

View File

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

View File

@@ -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({

View File

@@ -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);