fix(plugins): roll back failed register globals

This commit is contained in:
Ayaan Zaidi
2026-04-17 09:38:47 +05:30
parent 5c1d6feb33
commit 59d07f0ab4
3 changed files with 41 additions and 3 deletions

View File

@@ -396,6 +396,16 @@ export function listContextEngineIds(): string[] {
return [...getContextEngineRegistryState().engines.keys()];
}
export function clearContextEnginesForOwner(owner: string): void {
const normalizedOwner = requireContextEngineOwner(owner);
const registry = getContextEngineRegistryState().engines;
for (const [id, entry] of registry.entries()) {
if (entry.owner === normalizedOwner) {
registry.delete(id);
}
}
}
function describeResolvedContextEngineContractError(
engineId: string,
engine: unknown,

View File

@@ -3,6 +3,7 @@ import path from "node:path";
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import { listAgentHarnessIds } from "../agents/harness/registry.js";
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import { getContextEngineFactory, listContextEngineIds } from "../context-engine/registry.js";
import {
clearInternalHooks,
createInternalHookEvent,
@@ -14,6 +15,10 @@ import { withEnv } from "../test-utils/env.js";
import { clearPluginCommands, getPluginCommandSpecs } from "./command-registry-state.js";
import { getGlobalHookRunner, resetGlobalHookRunner } from "./hook-runner-global.js";
import { createHookRunner } from "./hooks.js";
import {
clearPluginInteractiveHandlers,
resolvePluginInteractiveNamespaceMatch,
} from "./interactive-registry.js";
import {
__testing,
clearPluginLoaderCache,
@@ -1770,7 +1775,7 @@ module.exports = { id: "throws-after-import", register() {} };`,
clearInternalHooks();
});
it("rolls back global hook and command side effects when registration fails", async () => {
it("rolls back global side effects when registration fails", async () => {
useNoBundledPlugins();
const plugin = writePlugin({
id: "failing-side-effects",
@@ -1790,6 +1795,16 @@ module.exports = { id: "throws-after-import", register() {} };`,
description: "Fail me",
handler: async () => ({ text: "nope" }),
});
api.registerInteractiveHandler({
channel: "slack",
namespace: "failme",
handle: async () => ({ handled: true }),
});
api.registerContextEngine("failme-context", () => ({
info: { id: "failme-context", name: "Failme Context" },
ingest: async () => {},
assemble: async () => ({ messages: [] }),
}));
throw new Error("boom");
},
};`,
@@ -1797,6 +1812,7 @@ module.exports = { id: "throws-after-import", register() {} };`,
clearInternalHooks();
clearPluginCommands();
clearPluginInteractiveHandlers();
const registry = loadOpenClawPlugins({
cache: false,
@@ -1815,6 +1831,9 @@ module.exports = { id: "throws-after-import", register() {} };`,
);
expect(getRegisteredEventKeys()).toEqual([]);
expect(getPluginCommandSpecs()).toEqual([]);
expect(resolvePluginInteractiveNamespaceMatch("slack", "failme:payload")).toBeNull();
expect(getContextEngineFactory("failme-context")).toBeUndefined();
expect(listContextEngineIds()).not.toContain("failme-context");
const event = createInternalHookEvent("gateway", "startup", "gateway:startup");
await triggerInternalHook(event);
@@ -1822,6 +1841,7 @@ module.exports = { id: "throws-after-import", register() {} };`,
clearInternalHooks();
clearPluginCommands();
clearPluginInteractiveHandlers();
});
it("can scope bundled provider loads to deepseek without hanging", () => {

View File

@@ -6,7 +6,10 @@ import {
import type { AgentHarness } from "../agents/harness/types.js";
import type { AnyAgentTool } from "../agents/tools/common.js";
import type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
import { registerContextEngineForOwner } from "../context-engine/registry.js";
import {
clearContextEnginesForOwner,
registerContextEngineForOwner,
} from "../context-engine/registry.js";
import type { OperatorScope } from "../gateway/operator-scopes.js";
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
import { registerInternalHook, unregisterInternalHook } from "../hooks/internal-hooks.js";
@@ -30,7 +33,10 @@ import {
} from "./compaction-provider.js";
import { normalizePluginHttpPath } from "./http-path.js";
import { findOverlappingPluginHttpRoute } from "./http-route-overlap.js";
import { registerPluginInteractiveHandler } from "./interactive-registry.js";
import {
clearPluginInteractiveHandlersForPlugin,
registerPluginInteractiveHandler,
} from "./interactive-registry.js";
import type { PluginDiagnostic } from "./manifest-types.js";
import {
getRegisteredMemoryEmbeddingProvider,
@@ -1430,6 +1436,8 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
}
clearPluginCommandsForPlugin(pluginId);
clearPluginInteractiveHandlersForPlugin(pluginId);
clearContextEnginesForOwner(`plugin:${pluginId}`);
const hookRollbackEntries = pluginHookRollback.get(pluginId) ?? [];
for (const entry of hookRollbackEntries.toReversed()) {