diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index b126fab7217..afc705ff0d8 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -245,6 +245,28 @@ function createManifestRegistryFixture() { providers: [], cliBackends: [], }, + { + id: "demo-global-startup-opt-out", + channels: [], + activation: { + onStartup: false, + }, + origin: "global", + enabledByDefault: undefined, + providers: [], + cliBackends: [], + }, + { + id: "demo-global-explicit-startup", + channels: [], + activation: { + onStartup: true, + }, + origin: "global", + enabledByDefault: undefined, + providers: [], + cliBackends: [], + }, ].map(withManifestLoadPaths), diagnostics: [], }; @@ -297,7 +319,12 @@ function createInstalledPluginRecordFixture( enabled: true, ...(record.enabledByDefault === true ? { enabledByDefault: true } : {}), startup: { - sidecar: record.channels.length === 0 && !hasRuntimeContractSurface(record) && !memory, + sidecar: + record.activation?.onStartup === true || + (record.activation?.onStartup === undefined && + record.channels.length === 0 && + !hasRuntimeContractSurface(record) && + !memory), memory, deferConfiguredChannelFullLoadUntilAfterListen: record.startupDeferConfiguredChannelFullLoadUntilAfterListen === true, @@ -623,6 +650,42 @@ describe("resolveGatewayStartupPluginIds", () => { }); }); + it("keeps deprecated implicit startup sidecar fallback for legacy plugins", () => { + expectStartupPluginIdsCase({ + config: createStartupConfig({ + enabledPluginIds: ["demo-global-sidecar"], + allowPluginIds: ["demo-global-sidecar"], + noConfiguredChannels: true, + memorySlot: "none", + }), + expected: ["demo-global-sidecar"], + }); + }); + + it("skips deprecated implicit startup sidecar fallback when activation.onStartup is false", () => { + expectStartupPluginIdsCase({ + config: createStartupConfig({ + enabledPluginIds: ["demo-global-startup-opt-out"], + allowPluginIds: ["demo-global-startup-opt-out"], + noConfiguredChannels: true, + memorySlot: "none", + }), + expected: [], + }); + }); + + it("loads explicit startup plugins when activation.onStartup is true", () => { + expectStartupPluginIdsCase({ + config: createStartupConfig({ + enabledPluginIds: ["demo-global-explicit-startup"], + allowPluginIds: ["demo-global-explicit-startup"], + noConfiguredChannels: true, + memorySlot: "none", + }), + expected: ["demo-global-explicit-startup"], + }); + }); + it("starts bundled sidecars selected by root config activation paths", () => { const rawConfig = { browser: { diff --git a/src/plugins/gateway-startup-plugin-ids.ts b/src/plugins/gateway-startup-plugin-ids.ts index c152735b83b..13890c3d6c8 100644 --- a/src/plugins/gateway-startup-plugin-ids.ts +++ b/src/plugins/gateway-startup-plugin-ids.ts @@ -65,8 +65,16 @@ function isGatewayStartupMemoryPlugin(plugin: InstalledPluginIndexRecord): boole return plugin.startup.memory; } -function isGatewayStartupSidecar(plugin: InstalledPluginIndexRecord): boolean { - return plugin.startup.sidecar; +/** + * @deprecated Compatibility fallback for plugins that do not declare + * `activation.onStartup`. Keep this path visible so we can remove it after + * plugin manifests migrate to explicit startup activation. + */ +function isDeprecatedLegacyImplicitStartupSidecar(params: { + plugin: InstalledPluginIndexRecord; + manifest: PluginManifestRecord | undefined; +}): boolean { + return params.plugin.startup.sidecar && params.manifest?.activation?.onStartup === undefined; } function resolveGatewayStartupDreamingPluginIds(config: OpenClawConfig): Set { @@ -108,12 +116,26 @@ function resolveMemorySlotStartupPluginId(params: { function shouldConsiderForGatewayStartup(params: { plugin: InstalledPluginIndexRecord; + manifest: PluginManifestRecord | undefined; startupDreamingPluginIds: ReadonlySet; memorySlotStartupPluginId?: string; }): boolean { - if (isGatewayStartupSidecar(params.plugin)) { + if (params.manifest?.activation?.onStartup === true) { return true; } + if (params.plugin.startup.sidecar) { + if (params.manifest?.activation?.onStartup === false) { + return false; + } + // Deprecated compatibility fallback: plugins without explicit startup + // activation metadata may still need startup import to register hooks or + // services. All plugins should declare activation.onStartup explicitly as + // we migrate away from implicit startup sidecar loading. + return isDeprecatedLegacyImplicitStartupSidecar({ + plugin: params.plugin, + manifest: params.manifest, + }); + } if (!isGatewayStartupMemoryPlugin(params.plugin)) { return false; } @@ -399,15 +421,6 @@ export function resolveGatewayStartupPluginIdsFromRegistry(params: { }); return activationState.enabled; } - if ( - !shouldConsiderForGatewayStartup({ - plugin, - startupDreamingPluginIds, - memorySlotStartupPluginId, - }) - ) { - return false; - } if ( canStartConfiguredRootPlugin({ plugin, @@ -419,6 +432,16 @@ export function resolveGatewayStartupPluginIdsFromRegistry(params: { ) { return true; } + if ( + !shouldConsiderForGatewayStartup({ + plugin, + manifest, + startupDreamingPluginIds, + memorySlotStartupPluginId, + }) + ) { + return false; + } const activationState = resolveEffectivePluginActivationState({ id: plugin.pluginId, origin: plugin.origin,