diff --git a/src/context-engine/registry.ts b/src/context-engine/registry.ts index 4979dfdfb69..8d13fc0777e 100644 --- a/src/context-engine/registry.ts +++ b/src/context-engine/registry.ts @@ -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, diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index aaab857e082..684aeb01735 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -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", () => { diff --git a/src/plugins/registry.ts b/src/plugins/registry.ts index 565b1edf3fe..b244473b4f2 100644 --- a/src/plugins/registry.ts +++ b/src/plugins/registry.ts @@ -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()) {