mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
fix(plugins): preserve external capability provider fallback (#76536)
* fix(plugins): preserve external capability provider fallback * docs: move changelog entry to avoid merge conflict --------- Co-authored-by: Clawdbot <clawdbot@apilab.us>
This commit is contained in:
@@ -151,6 +151,8 @@ Docs: https://docs.openclaw.ai
|
||||
- Memory-core: treat exhausted file watcher limits as non-fatal for builtin memory auto-sync while preserving fatal handling for unrelated disk-full errors. (#73357) Thanks @solodmd.
|
||||
- Providers/Ollama: restore catalog context-window forwarding as `num_ctx` for native `/api/chat` requests; fixes tool selection and context truncation regressions on models with catalog entries (qwen3, llama3, gemma3, …) when no explicit `params.num_ctx` was configured. Fixes #76117. (#76181) Thanks @openperf.
|
||||
|
||||
- Plugins/providers: preserve scoped cold-load fallback for enabled external manifest-contract capability providers missing from the startup registry, so providers such as Fish Audio can resolve on request without requiring `activation.onStartup` for correctness. (#76536) Thanks @Conan-Scott.
|
||||
|
||||
## 2026.5.2
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -156,7 +156,8 @@ function collectActiveRegistryLookups() {
|
||||
Boolean(
|
||||
options &&
|
||||
typeof options === "object" &&
|
||||
Object.hasOwn(options as Record<string, unknown>, "onlyPluginIds"),
|
||||
Object.hasOwn(options as Record<string, unknown>, "onlyPluginIds") &&
|
||||
!Object.hasOwn(options as Record<string, unknown>, "activate"),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -471,6 +472,62 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
expect(mocks.loadBundledCapabilityRuntimeRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cold-loads enabled external manifest-contract providers missing from startup registry", () => {
|
||||
const loaded = createEmptyPluginRegistry();
|
||||
loaded.speechProviders.push({
|
||||
pluginId: "fish-audio",
|
||||
pluginName: "Fish Audio",
|
||||
source: "test",
|
||||
provider: {
|
||||
id: "fish-audio",
|
||||
label: "Fish Audio",
|
||||
isConfigured: () => true,
|
||||
synthesize: async () => ({ kind: "audio", data: Buffer.from([]), mimeType: "audio/mpeg" }),
|
||||
},
|
||||
} as never);
|
||||
mocks.loadPluginRegistrySnapshot.mockReturnValue({
|
||||
plugins: [{ pluginId: "fish-audio", origin: "global", enabled: true }],
|
||||
});
|
||||
mocks.loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "fish-audio",
|
||||
origin: "global",
|
||||
enabledByDefault: false,
|
||||
contracts: { speechProviders: ["fish-audio"] },
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
mocks.resolveRuntimePluginRegistry.mockImplementation((options?: unknown) => {
|
||||
if (
|
||||
options &&
|
||||
typeof options === "object" &&
|
||||
(options as { activate?: unknown }).activate === false
|
||||
) {
|
||||
return loaded;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const provider = resolvePluginCapabilityProvider({
|
||||
key: "speechProviders",
|
||||
providerId: "fish-audio",
|
||||
});
|
||||
|
||||
expect(provider?.id).toBe("fish-audio");
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
onlyPluginIds: ["fish-audio"],
|
||||
});
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
activate: false,
|
||||
onlyPluginIds: ["fish-audio"],
|
||||
}),
|
||||
);
|
||||
expect(mocks.loadBundledCapabilityRuntimeRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses active non-speech capability providers even when cfg has explicit plugin entries", () => {
|
||||
const active = createEmptyPluginRegistry();
|
||||
active.mediaUnderstandingProviders.push({
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
withBundledPluginEnablementCompat,
|
||||
withBundledPluginVitestCompat,
|
||||
} from "./bundled-compat.js";
|
||||
import { resolvePluginRegistryLoadCacheKey, type PluginLoadOptions } from "./loader.js";
|
||||
import {
|
||||
resolvePluginRegistryLoadCacheKey,
|
||||
resolveRuntimePluginRegistry,
|
||||
type PluginLoadOptions,
|
||||
} from "./loader.js";
|
||||
import {
|
||||
hasManifestContractValue,
|
||||
isManifestPluginAvailableForControlPlane,
|
||||
@@ -414,13 +418,23 @@ function loadCapabilityProviderEntries<K extends CapabilityProviderRegistryKey>(
|
||||
loadOptions: PluginLoadOptions;
|
||||
requested?: Set<string>;
|
||||
}): PluginRegistry[K] {
|
||||
const registry = getLoadedRuntimePluginRegistry({
|
||||
const loadedRegistry = getLoadedRuntimePluginRegistry({
|
||||
env: params.loadOptions.env,
|
||||
loadOptions: params.loadOptions,
|
||||
workspaceDir: params.loadOptions.workspaceDir,
|
||||
requiredPluginIds: params.loadOptions.onlyPluginIds,
|
||||
});
|
||||
const entries = registry?.[params.key] ?? [];
|
||||
const loadedEntries = loadedRegistry?.[params.key] ?? [];
|
||||
const coldRegistry = loadedRegistry
|
||||
? undefined
|
||||
: resolveRuntimePluginRegistry(params.loadOptions);
|
||||
const coldEntries = coldRegistry?.[params.key] ?? [];
|
||||
const entries =
|
||||
loadedEntries.length > 0 && coldEntries.length > 0
|
||||
? mergeCapabilityProviderEntries(loadedEntries, coldEntries)
|
||||
: loadedEntries.length > 0
|
||||
? loadedEntries
|
||||
: coldEntries;
|
||||
const missingRequested =
|
||||
params.key === "speechProviders" && params.requested && params.requested.size > 0
|
||||
? new Set(params.requested)
|
||||
|
||||
Reference in New Issue
Block a user