mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
fix(plugins): reuse gateway boot registry for runtime ensures
Co-authored-by: Mark Ramos <6416874+markthebest12@users.noreply.github.com>
This commit is contained in:
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Slack/Assistant: accept Slack Assistant DM `message_changed` events when their metadata identifies the human sender, while continuing to drop self-authored bot edits. Fixes #55445. Thanks @AlfredPros.
|
||||
- Agents/failover: stop body-less HTTP 400/422 proxy failures from defaulting to `"format"` classification, so embedded retries surface the opaque provider failure instead of falling into a compaction loop. Fixes #66462. (#67024) Thanks @altaywtf and @HongzhuLiu.
|
||||
- Plugins/loader: use cached discovery-mode snapshot loads for read-only plugin capability lookups, keep snapshot caches isolated from active Gateway registries, and make same-plugin channel/HTTP route re-registration idempotent so repeated snapshot or hot-reload paths no longer rerun full plugin side effects or accumulate duplicate surfaces. Fixes #51781, #52031, #54181, and #57514. Thanks @livingghost, @okuyam2y, @ShionEria, and @bbshih.
|
||||
- Plugins/loader: reuse the compatible active Gateway registry for broad runtime plugin ensure calls after a gateway-bindable boot load, so non-bundled plugins no longer re-run `register()` during the same boot path. Fixes #69250. Thanks @markthebest12.
|
||||
|
||||
## 2026.4.24
|
||||
|
||||
|
||||
@@ -77,6 +77,10 @@ import {
|
||||
listImportedRuntimePluginIds,
|
||||
setActivePluginRegistry,
|
||||
} from "./runtime.js";
|
||||
import {
|
||||
__testing as runtimeRegistryLoaderTesting,
|
||||
ensurePluginRegistryLoaded,
|
||||
} from "./runtime/runtime-registry-loader.js";
|
||||
import type { PluginSdkResolutionPreference } from "./sdk-alias.js";
|
||||
let cachedBundledTelegramDir = "";
|
||||
let cachedBundledMemoryDir = "";
|
||||
@@ -832,6 +836,7 @@ function expectEscapingEntryRejected(params: {
|
||||
|
||||
afterEach(() => {
|
||||
clearRuntimeConfigSnapshot();
|
||||
runtimeRegistryLoaderTesting.resetPluginRegistryLoadedForTests();
|
||||
resetPluginLoaderTestStateForTest();
|
||||
});
|
||||
|
||||
@@ -3518,6 +3523,46 @@ module.exports = { id: "throws-after-import", register() {} };`,
|
||||
delete (globalThis as Record<string, unknown>)[marker];
|
||||
});
|
||||
|
||||
it("does not re-register non-bundled plugins after gateway-bindable boot loads", () => {
|
||||
useNoBundledPlugins();
|
||||
const marker = "__openclawGatewayBootRegisterCount";
|
||||
const plugin = writePlugin({
|
||||
id: "costclaw-boot-cache",
|
||||
filename: "costclaw-boot-cache.cjs",
|
||||
body: `module.exports = {
|
||||
id: "costclaw-boot-cache",
|
||||
register() {
|
||||
globalThis.${marker} = (globalThis.${marker} || 0) + 1;
|
||||
},
|
||||
};`,
|
||||
});
|
||||
const config = {
|
||||
plugins: {
|
||||
load: { paths: [plugin.file] },
|
||||
allow: ["costclaw-boot-cache"],
|
||||
entries: {
|
||||
"costclaw-boot-cache": { enabled: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
loadOpenClawPlugins({
|
||||
workspaceDir: plugin.dir,
|
||||
config,
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
});
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "all",
|
||||
workspaceDir: plugin.dir,
|
||||
config,
|
||||
});
|
||||
|
||||
expect((globalThis as Record<string, unknown>)[marker]).toBe(1);
|
||||
delete (globalThis as Record<string, unknown>)[marker];
|
||||
});
|
||||
|
||||
it("re-initializes global hook runner when serving registry from cache", () => {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||
const plugin = writePlugin({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createEmptyPluginRegistry } from "../registry.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
loadOpenClawPlugins: vi.fn<typeof import("../loader.js").loadOpenClawPlugins>(),
|
||||
resolveRuntimePluginRegistry: vi.fn<typeof import("../loader.js").resolveRuntimePluginRegistry>(),
|
||||
getActivePluginRegistry: vi.fn<typeof import("../runtime.js").getActivePluginRegistry>(),
|
||||
resolveConfiguredChannelPluginIds:
|
||||
vi.fn<typeof import("../channel-plugin-ids.js").resolveConfiguredChannelPluginIds>(),
|
||||
@@ -24,6 +25,8 @@ let resetPluginRegistryLoadedForTests: typeof import("./runtime-registry-loader.
|
||||
vi.mock("../loader.js", () => ({
|
||||
loadOpenClawPlugins: (...args: Parameters<typeof mocks.loadOpenClawPlugins>) =>
|
||||
mocks.loadOpenClawPlugins(...args),
|
||||
resolveRuntimePluginRegistry: (...args: Parameters<typeof mocks.resolveRuntimePluginRegistry>) =>
|
||||
mocks.resolveRuntimePluginRegistry(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../runtime.js", () => ({
|
||||
@@ -61,6 +64,7 @@ describe("ensurePluginRegistryLoaded", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.loadOpenClawPlugins.mockReset();
|
||||
mocks.resolveRuntimePluginRegistry.mockReset();
|
||||
mocks.getActivePluginRegistry.mockReset();
|
||||
mocks.resolveConfiguredChannelPluginIds.mockReset();
|
||||
mocks.resolveChannelPluginIds.mockReset();
|
||||
@@ -70,6 +74,10 @@ describe("ensurePluginRegistryLoaded", () => {
|
||||
resetPluginRegistryLoadedForTests();
|
||||
|
||||
mocks.getActivePluginRegistry.mockReturnValue(createEmptyPluginRegistry());
|
||||
mocks.loadOpenClawPlugins.mockReturnValue(createEmptyPluginRegistry());
|
||||
mocks.resolveRuntimePluginRegistry.mockImplementation(
|
||||
(...args: Parameters<typeof mocks.loadOpenClawPlugins>) => mocks.loadOpenClawPlugins(...args),
|
||||
);
|
||||
mocks.applyPluginAutoEnable.mockImplementation((params) => ({
|
||||
config:
|
||||
params.config && typeof params.config === "object"
|
||||
@@ -255,4 +263,26 @@ describe("ensurePluginRegistryLoaded", () => {
|
||||
(mocks.loadOpenClawPlugins.mock.calls[0]?.[0] as { onlyPluginIds?: string[] }).onlyPluginIds,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("reuses a compatible active registry instead of forcing a broad reload", () => {
|
||||
const activeRegistry = createEmptyPluginRegistry();
|
||||
mocks.resolveRuntimePluginRegistry.mockReturnValue(activeRegistry);
|
||||
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "all",
|
||||
config: { plugins: { allow: ["demo"] } } as never,
|
||||
});
|
||||
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
throwOnLoadError: true,
|
||||
}),
|
||||
);
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
onlyPluginIds: expect.any(Array),
|
||||
}),
|
||||
);
|
||||
expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
resolveChannelPluginIds,
|
||||
resolveConfiguredChannelPluginIds,
|
||||
} from "../channel-plugin-ids.js";
|
||||
import { loadOpenClawPlugins } from "../loader.js";
|
||||
import { loadOpenClawPlugins, resolveRuntimePluginRegistry } from "../loader.js";
|
||||
import {
|
||||
hasExplicitPluginIdScope,
|
||||
hasNonEmptyPluginIdScope,
|
||||
@@ -73,6 +73,16 @@ function shouldForwardChannelScope(params: {
|
||||
return !params.scopedLoad && params.scope === "configured-channels";
|
||||
}
|
||||
|
||||
function resolveOrLoadRuntimePluginRegistry(
|
||||
loadOptions: Parameters<typeof loadOpenClawPlugins>[0],
|
||||
): void {
|
||||
// Prefer the runtime resolver so broad ensures can reuse compatible active
|
||||
// registries, including gateway-bindable startup registries.
|
||||
if (!resolveRuntimePluginRegistry(loadOptions)) {
|
||||
loadOpenClawPlugins(loadOptions);
|
||||
}
|
||||
}
|
||||
|
||||
export function ensurePluginRegistryLoaded(options?: {
|
||||
scope?: PluginRegistryScope;
|
||||
config?: OpenClawConfig;
|
||||
@@ -132,23 +142,22 @@ export function ensurePluginRegistryLoaded(options?: {
|
||||
pluginIds: expectedChannelPluginIds,
|
||||
}) ?? context.activationSourceConfig)
|
||||
: context.activationSourceConfig;
|
||||
loadOpenClawPlugins(
|
||||
buildPluginRuntimeLoadOptionsFromValues(
|
||||
{
|
||||
...context,
|
||||
config: scopedConfig,
|
||||
activationSourceConfig: scopedActivationSourceConfig,
|
||||
},
|
||||
{
|
||||
throwOnLoadError: true,
|
||||
...(hasExplicitPluginIdScope(requestedPluginIds) ||
|
||||
shouldForwardChannelScope({ scope, scopedLoad }) ||
|
||||
hasNonEmptyPluginIdScope(expectedChannelPluginIds)
|
||||
? { onlyPluginIds: expectedChannelPluginIds }
|
||||
: {}),
|
||||
},
|
||||
),
|
||||
const loadOptions = buildPluginRuntimeLoadOptionsFromValues(
|
||||
{
|
||||
...context,
|
||||
config: scopedConfig,
|
||||
activationSourceConfig: scopedActivationSourceConfig,
|
||||
},
|
||||
{
|
||||
throwOnLoadError: true,
|
||||
...(hasExplicitPluginIdScope(requestedPluginIds) ||
|
||||
shouldForwardChannelScope({ scope, scopedLoad }) ||
|
||||
hasNonEmptyPluginIdScope(expectedChannelPluginIds)
|
||||
? { onlyPluginIds: expectedChannelPluginIds }
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
resolveOrLoadRuntimePluginRegistry(loadOptions);
|
||||
if (!scopedLoad) {
|
||||
pluginRegistryLoaded = scope;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user