fix(plugins): preserve gateway hook runner

Co-authored-by: lanzhi-lee <36190508+lanzhi-lee@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-04-25 00:28:41 +01:00
parent 2b5c719a62
commit 0376987691
3 changed files with 74 additions and 3 deletions

View File

@@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai
- 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.
- Plugins/hooks: keep the gateway-bindable hook runner installed when later default-mode plugin loads activate a different registry, preserving Gateway subagent lifecycle hooks across runtime cache misses. Fixes #63166.
## 2026.4.24

View File

@@ -23,7 +23,11 @@ import {
} from "../tasks/detached-task-runtime-state.js";
import { withEnv } from "../test-utils/env.js";
import { clearPluginCommands, getPluginCommandSpecs } from "./command-registry-state.js";
import { getGlobalHookRunner, resetGlobalHookRunner } from "./hook-runner-global.js";
import {
getGlobalHookRunner,
getGlobalPluginRegistry,
resetGlobalHookRunner,
} from "./hook-runner-global.js";
import { createHookRunner } from "./hooks.js";
import {
clearPluginInteractiveHandlerRegistrations,
@@ -3594,6 +3598,66 @@ module.exports = { id: "throws-after-import", register() {} };`,
resetGlobalHookRunner();
});
it("preserves the gateway-bindable hook runner across later default-mode activating loads", () => {
useNoBundledPlugins();
const gatewayPlugin = writePlugin({
id: "gateway-hook-surface",
filename: "gateway-hook-surface.cjs",
body: `module.exports = { id: "gateway-hook-surface", register(api) {
api.on("subagent_ended", () => undefined);
} };`,
});
const defaultPlugin = writePlugin({
id: "default-hook-surface",
filename: "default-hook-surface.cjs",
body: `module.exports = { id: "default-hook-surface", register(api) {
api.on("message_sent", () => undefined);
} };`,
});
const gatewayRegistry = loadOpenClawPlugins({
workspaceDir: gatewayPlugin.dir,
config: {
plugins: {
load: { paths: [gatewayPlugin.file] },
allow: ["gateway-hook-surface"],
entries: {
"gateway-hook-surface": {
enabled: true,
hooks: { allowConversationAccess: true },
},
},
},
},
runtimeOptions: {
allowGatewaySubagentBinding: true,
},
});
expect(getGlobalPluginRegistry()).toBe(gatewayRegistry);
expect(getGlobalHookRunner()?.hasHooks("subagent_ended")).toBe(true);
const defaultRegistry = loadOpenClawPlugins({
workspaceDir: defaultPlugin.dir,
config: {
plugins: {
load: { paths: [defaultPlugin.file] },
allow: ["default-hook-surface"],
entries: {
"default-hook-surface": {
enabled: true,
hooks: { allowConversationAccess: true },
},
},
},
},
});
expect(getActivePluginRegistry()).toBe(defaultRegistry);
expect(getGlobalPluginRegistry()).toBe(gatewayRegistry);
expect(getGlobalHookRunner()?.hasHooks("subagent_ended")).toBe(true);
expect(getGlobalHookRunner()?.hasHooks("message_sent")).toBe(false);
});
it.each([
{
name: "does not reuse cached bundled plugin registries across env changes",

View File

@@ -59,7 +59,7 @@ import {
type PluginActivationState,
} from "./config-state.js";
import { discoverOpenClawPlugins } from "./discovery.js";
import { initializeGlobalHookRunner } from "./hook-runner-global.js";
import { getGlobalHookRunner, initializeGlobalHookRunner } from "./hook-runner-global.js";
import {
clearPluginInteractiveHandlers,
listPluginInteractiveHandlers,
@@ -1953,8 +1953,14 @@ function activatePluginRegistry(
runtimeSubagentMode: "default" | "explicit" | "gateway-bindable",
workspaceDir?: string,
): void {
const preserveGatewayHookRunner =
runtimeSubagentMode === "default" &&
getActivePluginRuntimeSubagentMode() === "gateway-bindable" &&
getGlobalHookRunner() !== null;
setActivePluginRegistry(registry, cacheKey, runtimeSubagentMode, workspaceDir);
initializeGlobalHookRunner(registry);
if (!preserveGatewayHookRunner) {
initializeGlobalHookRunner(registry);
}
}
export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegistry {