From a85cd65775fc7d840479ec308e4e5f4db97221c2 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 16 May 2026 18:35:00 +0800 Subject: [PATCH] fix(plugins): deprecate deactivate hook alias --- CHANGELOG.md | 2 +- docs/plugins/compatibility.md | 2 ++ docs/plugins/hooks.md | 6 +++--- docs/plugins/sdk-migration.md | 23 +++++++++++++++++++++++ src/plugins/compat/registry.test.ts | 5 +++++ src/plugins/compat/registry.ts | 16 ++++++++++++++++ src/plugins/hook-types.ts | 5 ++++- src/plugins/loader.test.ts | 3 ++- src/plugins/registry.ts | 13 ++++++++++++- 9 files changed, 68 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b24935f9299..0aebd1f9d1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ Docs: https://docs.openclaw.ai - Telegram: persist polling updates through restart replay so queued same-topic messages resume in order instead of losing context after a gateway restart. (#82256) Thanks @VACInc. - Gateway/Gmail: abort in-flight Gmail watcher startup and hot-reload restarts before shutdown so reloads cannot spawn `gog serve` after the Gateway is closing. Thanks @frankekn. - MCP plugin tools: forward host MCP `tools/call` `AbortSignal` through `createPluginToolsMcpHandlers().callTool` into plugin `tool.execute`, so host cancellation actually cancels in-flight plugin tool calls instead of letting them run to completion. Fixes #82424. (#82443) Thanks @joshavant. -- Plugins: accept `api.on("deactivate")` as a compatibility alias for `gateway_stop`, so external plugin cleanup handlers run on Gateway shutdown instead of being ignored as unknown hooks. +- Plugins: accept deprecated `api.on("deactivate")` registrations as a dated compatibility alias for `gateway_stop`, so external plugin cleanup handlers run on Gateway shutdown while authors get migration guidance. - Media: ignore image MIME and filename hints when bytes sniff as generic containers, so zip/octet-stream payloads mislabeled as images do not become local image media or keep image file extensions when staged. - Update/doctor: avoid materializing `groupAllowFrom` for channel schemas that reject it, so package-swap doctor repairs do not fail on externalized Slack configs. - Gateway/media: prevent image filenames from overriding generic non-image byte sniffing, so zip/octet-stream payloads mislabeled as images are offloaded or rejected before they become inline image attachments. diff --git a/docs/plugins/compatibility.md b/docs/plugins/compatibility.md index 6218a68357e..b637e2b04d1 100644 --- a/docs/plugins/compatibility.md +++ b/docs/plugins/compatibility.md @@ -112,6 +112,8 @@ Current compatibility records include: - legacy broad SDK imports such as `openclaw/plugin-sdk/compat` - legacy hook-only plugin shapes and `before_agent_start` +- legacy `api.on("deactivate", ...)` cleanup hook names while plugins migrate to + `gateway_stop` - legacy `activate(api)` plugin entrypoints while plugins migrate to `register(api)` - legacy SDK aliases such as `openclaw/extension-api`, diff --git a/docs/plugins/hooks.md b/docs/plugins/hooks.md index 8ba45000740..e77e94fba3a 100644 --- a/docs/plugins/hooks.md +++ b/docs/plugins/hooks.md @@ -145,7 +145,7 @@ observation-only. **Lifecycle** - `gateway_start` / `gateway_stop` - start or stop plugin-owned services with the Gateway -- `deactivate` - compatibility alias for `gateway_stop`; prefer `gateway_stop` in new plugins +- `deactivate` - deprecated compatibility alias for `gateway_stop`; use `gateway_stop` in new plugins - `cron_changed` - observe gateway-owned cron lifecycle changes (added, updated, removed, started, finished, scheduled) - **`before_install`** - inspect skill or plugin install scans and optionally block @@ -438,8 +438,8 @@ before the next major release: - **`before_agent_start`** remains for compatibility. New plugins should use `before_model_resolve` and `before_prompt_build` instead of the combined phase. -- **`deactivate`** remains as a cleanup compatibility alias. New plugins should - use `gateway_stop`. +- **`deactivate`** remains as a deprecated cleanup compatibility alias until + after 2026-08-16. New plugins should use `gateway_stop`. - **`onResolution` in `before_tool_call`** now uses the typed `PluginApprovalResolution` union (`allow-once` / `allow-always` / `deny` / `timeout` / `cancelled`) instead of a free-form `string`. diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index cc08df12f93..f9d1bc1594d 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -757,6 +757,29 @@ canonical replacement. + + **Old**: `api.on("deactivate", handler)`. + + **New**: `api.on("gateway_stop", handler)`. The event and context are the + same shutdown cleanup contract; only the hook name changes. + + ```typescript + // Before + api.on("deactivate", async (event, ctx) => { + await stopPluginService(ctx); + }); + + // After + api.on("gateway_stop", async (event, ctx) => { + await stopPluginService(ctx); + }); + ``` + + `deactivate` remains wired as a deprecated compatibility alias until after + 2026-08-16. + + + Four discovery type aliases are now thin wrappers over the catalog-era types: diff --git a/src/plugins/compat/registry.test.ts b/src/plugins/compat/registry.test.ts index c5da9cb02d4..d963edc8b19 100644 --- a/src/plugins/compat/registry.test.ts +++ b/src/plugins/compat/registry.test.ts @@ -135,6 +135,11 @@ const knownDeprecatedSurfaceMarkers = [ file: "src/plugin-sdk/compat.ts", marker: "@deprecated Use `openclaw/plugin-sdk/channel-message`.", }, + { + code: "legacy-deactivate-hook-alias", + file: "src/plugins/hook-types.ts", + marker: "@deprecated Use gateway_stop", + }, { code: "channel-route-key-aliases", file: "src/plugin-sdk/channel-route.ts", diff --git a/src/plugins/compat/registry.ts b/src/plugins/compat/registry.ts index 4161a828b9e..48cee0aa639 100644 --- a/src/plugins/compat/registry.ts +++ b/src/plugins/compat/registry.ts @@ -23,6 +23,22 @@ export const PLUGIN_COMPAT_RECORDS = [ releaseNote: "Legacy `before_agent_start` hook compatibility remains wired while plugins migrate to modern hook stages.", }, + { + code: "legacy-deactivate-hook-alias", + status: "deprecated", + owner: "sdk", + introduced: "2026-05-16", + deprecated: "2026-05-16", + warningStarts: "2026-05-16", + removeAfter: "2026-08-16", + replacement: "`gateway_stop` hook", + docsPath: "/plugins/hooks#upcoming-deprecations", + surfaces: ["api.on(\"deactivate\", ...)", "plugin typed hook registration"], + diagnostics: ["plugin runtime compatibility warning"], + tests: ["src/plugins/loader.test.ts"], + releaseNote: + "`api.on(\"deactivate\", ...)` remains wired as a deprecated compatibility alias while plugins migrate to `gateway_stop`.", + }, { code: "hook-only-plugin-shape", status: "active", diff --git a/src/plugins/hook-types.ts b/src/plugins/hook-types.ts index 9a9555c8183..57add9558f1 100644 --- a/src/plugins/hook-types.ts +++ b/src/plugins/hook-types.ts @@ -94,6 +94,7 @@ export type PluginHookName = | "subagent_delivery_target" | "subagent_spawned" | "subagent_ended" + /** @deprecated Use gateway_stop. */ | "deactivate" | "gateway_start" | "gateway_stop" @@ -1003,11 +1004,13 @@ export type PluginHookHandlerMap = { ctx: PluginHookSubagentContext, ) => Promise | void; /** - * Compatibility alias for gateway_stop. + * Deprecated compatibility alias for gateway_stop. * * New plugins should register gateway_stop directly; the loader normalizes * deactivate registrations onto gateway_stop so cleanup handlers still run * during Gateway shutdown. + * + * @deprecated Use gateway_stop. */ deactivate: ( event: PluginHookGatewayStopEvent, diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 542ae99d7a0..f201325844d 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -5939,7 +5939,8 @@ module.exports = { registry.diagnostics.some( (diag) => diag.pluginId === "legacy-deactivate-hook" && - diag.message === 'typed hook "deactivate" is a compatibility alias for "gateway_stop"', + diag.message === + 'typed hook "deactivate" is deprecated (legacy-deactivate-hook-alias); use "gateway_stop". This compatibility alias will be removed after 2026-08-16.', ), ).toBe(true); }); diff --git a/src/plugins/registry.ts b/src/plugins/registry.ts index 84d52bb2e60..1c784af7e1d 100644 --- a/src/plugins/registry.ts +++ b/src/plugins/registry.ts @@ -51,6 +51,7 @@ import { import { buildPluginApi } from "./api-builder.js"; import { normalizeRegisteredChannelPlugin } from "./channel-validation.js"; import { CODEX_APP_SERVER_EXTENSION_RUNTIME_ID } from "./codex-app-server-extension-factory.js"; +import { getPluginCompatRecord } from "./compat/registry.js"; import type { CodexAppServerExtensionFactory } from "./codex-app-server-extension-types.js"; import { isReservedCommandName, @@ -187,6 +188,16 @@ export type PluginHttpRouteRegistration = RegistryTypesPluginHttpRouteRegistrati }; const GATEWAY_METHOD_DISPATCH_CONTRACT = "authenticated-request"; +const LEGACY_DEACTIVATE_HOOK_ALIAS_COMPAT = getPluginCompatRecord("legacy-deactivate-hook-alias"); + +function formatLegacyDeactivateHookAliasDiagnostic(): string { + const removeAfter = + LEGACY_DEACTIVATE_HOOK_ALIAS_COMPAT.removeAfter ?? "a future breaking release"; + return ( + `typed hook "deactivate" is deprecated (${LEGACY_DEACTIVATE_HOOK_ALIAS_COMPAT.code}); ` + + `use "gateway_stop". This compatibility alias will be removed after ${removeAfter}.` + ); +} type PluginOwnedProviderRegistration = { pluginId: string; @@ -2304,7 +2315,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { level: "warn", pluginId: record.id, source: record.source, - message: 'typed hook "deactivate" is a compatibility alias for "gateway_stop"', + message: formatLegacyDeactivateHookAliasDiagnostic(), }); } let effectiveHandler = handler;