diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c07cbf31f0..8b41b3ab3b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 327966e0ec9..e75991ee27c 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -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", diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index d544204751c..d04ab2a632f 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -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 {