diff --git a/src/agents/runtime-plugins.test.ts b/src/agents/runtime-plugins.test.ts index 659de14fd050..74af31517dd4 100644 --- a/src/agents/runtime-plugins.test.ts +++ b/src/agents/runtime-plugins.test.ts @@ -8,12 +8,17 @@ const hoisted = vi.hoisted(() => ({ () => "default", ), getActivePluginRegistryWorkspaceDir: vi.fn<() => string | undefined>(() => undefined), + getActiveRuntimePluginRegistry: vi.fn<() => unknown>(() => null), })); vi.mock("../plugins/current-plugin-metadata-snapshot.js", () => ({ getCurrentPluginMetadataSnapshot: hoisted.getCurrentPluginMetadataSnapshot, })); +vi.mock("../plugins/active-runtime-registry.js", () => ({ + getActiveRuntimePluginRegistry: hoisted.getActiveRuntimePluginRegistry, +})); + vi.mock("../plugins/runtime/standalone-runtime-registry-loader.js", () => ({ ensureStandaloneRuntimePluginRegistryLoaded: hoisted.ensureStandaloneRuntimePluginRegistryLoaded, })); @@ -36,6 +41,8 @@ describe("ensureRuntimePluginsLoaded", () => { hoisted.getActivePluginRuntimeSubagentMode.mockReturnValue("default"); hoisted.getActivePluginRegistryWorkspaceDir.mockReset(); hoisted.getActivePluginRegistryWorkspaceDir.mockReturnValue(undefined); + hoisted.getActiveRuntimePluginRegistry.mockReset(); + hoisted.getActiveRuntimePluginRegistry.mockReturnValue(null); vi.resetModules(); ({ ensureRuntimePluginsLoaded } = await import("./runtime-plugins.js")); }); @@ -52,6 +59,19 @@ describe("ensureRuntimePluginsLoaded", () => { expect(hoisted.ensureStandaloneRuntimePluginRegistryLoaded).toHaveBeenCalledTimes(1); }); + it("skips loading entirely when a runtime plugin registry is already active", () => { + hoisted.getActiveRuntimePluginRegistry.mockReturnValue({}); + + ensureRuntimePluginsLoaded({ + config: {} as never, + workspaceDir: "/tmp/workspace", + allowGatewaySubagentBinding: true, + }); + + expect(hoisted.getCurrentPluginMetadataSnapshot).not.toHaveBeenCalled(); + expect(hoisted.ensureStandaloneRuntimePluginRegistryLoaded).not.toHaveBeenCalled(); + }); + it("resolves runtime plugins through the shared runtime helper", () => { ensureRuntimePluginsLoaded({ config: {} as never, diff --git a/src/agents/runtime-plugins.ts b/src/agents/runtime-plugins.ts index 45049a7173da..650b48094463 100644 --- a/src/agents/runtime-plugins.ts +++ b/src/agents/runtime-plugins.ts @@ -3,6 +3,7 @@ * plugin IDs from metadata scope the load when available. */ import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { getActiveRuntimePluginRegistry } from "../plugins/active-runtime-registry.js"; import { normalizePluginsConfig } from "../plugins/config-state.js"; import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js"; import { getActivePluginRuntimeSubagentMode } from "../plugins/runtime.js"; @@ -41,6 +42,13 @@ export function ensureRuntimePluginsLoaded(params: { if (params.config && !normalizePluginsConfig(params.config.plugins).enabled) { return; } + // Activating a replacement registry retires the active one and runs its + // plugin host cleanup, which cron.remove()s persistent plugin-scheduled + // jobs the replacement hasn't re-registered yet (async, still in flight). + // Once a registry is active there is nothing left for this call to do. + if (getActiveRuntimePluginRegistry()) { + return; + } const workspaceDir = typeof params.workspaceDir === "string" && params.workspaceDir.trim() ? resolveUserPath(params.workspaceDir)