fix(plugins): preserve live hook registry during gateway runs

This commit is contained in:
Vincent Koc
2026-03-23 01:00:08 -07:00
parent 9105b3723d
commit d22279d2e8
9 changed files with 198 additions and 63 deletions

View File

@@ -159,6 +159,7 @@ function getHooksForNameAndPlugin<K extends PluginHookName>(
export function createHookRunner(registry: PluginRegistry, options: HookRunnerOptions = {}) {
const logger = options.logger;
const catchErrors = options.catchErrors ?? true;
const hookCheckpointLogsEnabled = process.env.OPENCLAW_PLUGIN_CHECKPOINTS === "1";
const mergeBeforeModelResolve = (
acc: PluginHookBeforeModelResolveResult | undefined,
@@ -250,9 +251,17 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
}
logger?.debug?.(`[hooks] running ${hookName} (${hooks.length} handlers)`);
if (hookCheckpointLogsEnabled) {
logger?.warn(
`[hooks][checkpoints] dispatch ${hookName} handlers=${hooks.map((hook) => hook.pluginId).join(",")}`,
);
}
const promises = hooks.map(async (hook) => {
try {
if (hookCheckpointLogsEnabled) {
logger?.warn(`[hooks][checkpoints] invoke ${hookName} plugin=${hook.pluginId}`);
}
await (hook.handler as (event: unknown, ctx: unknown) => Promise<void>)(event, ctx);
} catch (err) {
handleHookError({ hookName, pluginId: hook.pluginId, error: err });

View File

@@ -1,4 +1,3 @@
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
import { createEmptyPluginRegistry } from "./registry-empty.js";
import type { PluginRegistry } from "./registry.js";
@@ -12,15 +11,33 @@ type RegistryState = {
version: number;
};
const state = resolveGlobalSingleton<RegistryState>(REGISTRY_STATE, () => ({
registry: createEmptyPluginRegistry(),
httpRouteRegistry: null,
httpRouteRegistryPinned: false,
key: null,
version: 0,
}));
const state: RegistryState = (() => {
const globalState = globalThis as typeof globalThis & {
[REGISTRY_STATE]?: RegistryState;
};
if (!globalState[REGISTRY_STATE]) {
globalState[REGISTRY_STATE] = {
registry: createEmptyPluginRegistry(),
httpRouteRegistry: null,
httpRouteRegistryPinned: false,
key: null,
version: 0,
};
}
return globalState[REGISTRY_STATE];
})();
export function setActivePluginRegistry(registry: PluginRegistry, cacheKey?: string) {
if (process.env.OPENCLAW_PLUGIN_CHECKPOINTS === "1") {
const stack = new Error().stack
?.split("\n")
.slice(2, 5)
.map((line) => line.trim())
.join(" | ");
console.warn(
`[plugins][checkpoints] activate registry key=${cacheKey ?? "none"} plugins=${registry.plugins.length} typedHooks=${registry.typedHooks.length}${stack ? ` caller=${stack}` : ""}`,
);
}
state.registry = registry;
if (!state.httpRouteRegistryPinned) {
state.httpRouteRegistry = registry;

View File

@@ -5,6 +5,7 @@ import type { PluginRegistry } from "./registry.js";
import type { OpenClawPluginServiceContext, PluginLogger } from "./types.js";
const log = createSubsystemLogger("plugins");
const pluginCheckpointLogsEnabled = process.env.OPENCLAW_PLUGIN_CHECKPOINTS === "1";
function createPluginLogger(): PluginLogger {
return {
@@ -47,8 +48,18 @@ export async function startPluginServices(params: {
for (const entry of params.registry.services) {
const service = entry.service;
const typedHookCountBefore = params.registry.typedHooks.length;
try {
await service.start(serviceContext);
if (pluginCheckpointLogsEnabled) {
const newTypedHooks = params.registry.typedHooks
.slice(typedHookCountBefore)
.filter((hook) => hook.pluginId === entry.pluginId)
.map((hook) => hook.hookName);
log.warn(
`[plugins][checkpoints] service started (${service.id}, plugin=${entry.pluginId}) typedHooksAdded=${newTypedHooks.length}${newTypedHooks.length > 0 ? ` hooks=${newTypedHooks.join(",")}` : ""}`,
);
}
running.push({
id: service.id,
stop: service.stop ? () => service.stop?.(serviceContext) : undefined,

View File

@@ -4,6 +4,7 @@ import { createSubsystemLogger } from "../logging/subsystem.js";
import { applyTestPluginDefaults, normalizePluginsConfig } from "./config-state.js";
import { loadOpenClawPlugins } from "./loader.js";
import { createPluginLoaderLogger } from "./logger.js";
import { getActivePluginRegistry, getActivePluginRegistryKey } from "./runtime.js";
import type { OpenClawPluginToolContext } from "./types.js";
const log = createSubsystemLogger("plugins");
@@ -59,17 +60,21 @@ export function resolvePluginTools(params: {
return [];
}
const registry = loadOpenClawPlugins({
config: effectiveConfig,
workspaceDir: params.context.workspaceDir,
runtimeOptions: params.allowGatewaySubagentBinding
? {
allowGatewaySubagentBinding: true,
}
: undefined,
env,
logger: createPluginLoaderLogger(log),
});
const activeRegistry = getActivePluginRegistry();
const registry =
getActivePluginRegistryKey() && activeRegistry
? activeRegistry
: loadOpenClawPlugins({
config: effectiveConfig,
workspaceDir: params.context.workspaceDir,
runtimeOptions: params.allowGatewaySubagentBinding
? {
allowGatewaySubagentBinding: true,
}
: undefined,
env,
logger: createPluginLoaderLogger(log),
});
const tools: AnyAgentTool[] = [];
const existing = params.existingToolNames ?? new Set<string>();