From f14aa65bcc5831c5096df27e6bd11f23b8fc56ff Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 25 Apr 2026 05:50:00 -0700 Subject: [PATCH] fix(plugins): refresh registry after chat toggles --- CHANGELOG.md | 1 + src/auto-reply/reply/commands-plugins.test.ts | 29 +++++++++++++++++++ src/auto-reply/reply/commands-plugins.ts | 15 +++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b83563edbe..95cbb2b8260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai - Plugins/CLI: make `openclaw plugins list` read the cold persisted registry snapshot by default, leaving module-aware diagnostics to `plugins doctor` and `plugins inspect`. Thanks @vincentkoc. - Plugins/startup: move gateway startup plugin planning onto the versioned cold registry index, with postinstall repair for older registry files that predate startup metadata. Thanks @vincentkoc. - Providers/plugins: resolve provider ownership, provider discovery scopes, and catalog-hook provider ids from the cold plugin registry instead of rescanning manifests on those paths. Thanks @vincentkoc. +- Plugins/chat commands: refresh the persisted plugin registry after `/plugins enable` and `/plugins disable`, matching the CLI mutation path. Thanks @vincentkoc. - Diagnostics/OTEL: add bounded outbound message delivery lifecycle diagnostics and export them as low-cardinality delivery spans/metrics without message body, recipient, room, or media-path data. (#71471) Thanks @vincentkoc and @jlapenna. - Diagnostics/OTEL: emit bounded exec-process diagnostics and export them as `openclaw.exec` spans without exposing command text, working directories, or container identifiers. (#71451) Thanks @vincentkoc and @jlapenna. - Diagnostics/OTEL: support `OPENCLAW_OTEL_PRELOADED=1` so the plugin can reuse an already-registered OpenTelemetry SDK while keeping OpenClaw diagnostic listeners wired. (#71450) Thanks @vincentkoc and @jlapenna. diff --git a/src/auto-reply/reply/commands-plugins.test.ts b/src/auto-reply/reply/commands-plugins.test.ts index c7ecf1e695e..c298d7d06cf 100644 --- a/src/auto-reply/reply/commands-plugins.test.ts +++ b/src/auto-reply/reply/commands-plugins.test.ts @@ -11,6 +11,7 @@ const buildPluginDiagnosticsReportMock = vi.hoisted(() => vi.fn()); const buildPluginInspectReportMock = vi.hoisted(() => vi.fn()); const buildAllPluginInspectReportsMock = vi.hoisted(() => vi.fn()); const formatPluginCompatibilityNoticeMock = vi.hoisted(() => vi.fn(() => "ok")); +const refreshPluginRegistryAfterConfigMutationMock = vi.hoisted(() => vi.fn(async () => undefined)); vi.mock("../../cli/npm-resolution.js", () => ({ buildNpmInstallRecordFields: vi.fn(), @@ -27,6 +28,10 @@ vi.mock("../../cli/plugins-install-persist.js", () => ({ persistPluginInstall: vi.fn(async () => undefined), })); +vi.mock("../../cli/plugins-registry-refresh.js", () => ({ + refreshPluginRegistryAfterConfigMutation: refreshPluginRegistryAfterConfigMutationMock, +})); + vi.mock("../../config/config.js", () => ({ readConfigFileSnapshot: readConfigFileSnapshotMock, validateConfigObjectWithPlugins: validateConfigObjectWithPluginsMock, @@ -208,6 +213,18 @@ describe("handlePluginsCommand", () => { }), }), ); + expect(refreshPluginRegistryAfterConfigMutationMock).toHaveBeenLastCalledWith( + expect.objectContaining({ + reason: "policy-changed", + config: expect.objectContaining({ + plugins: expect.objectContaining({ + entries: expect.objectContaining({ + superpowers: expect.objectContaining({ enabled: true }), + }), + }), + }), + }), + ); const disableParams = buildPluginsParams("/plugins disable superpowers", buildCfg()); disableParams.command.senderIsOwner = true; @@ -223,6 +240,18 @@ describe("handlePluginsCommand", () => { }), }), ); + expect(refreshPluginRegistryAfterConfigMutationMock).toHaveBeenLastCalledWith( + expect.objectContaining({ + reason: "policy-changed", + config: expect.objectContaining({ + plugins: expect.objectContaining({ + entries: expect.objectContaining({ + superpowers: expect.objectContaining({ enabled: false }), + }), + }), + }), + }), + ); }); it("resolves write targets by runtime-derived plugin name", async () => { diff --git a/src/auto-reply/reply/commands-plugins.ts b/src/auto-reply/reply/commands-plugins.ts index 2b75ddf424e..adc67152c06 100644 --- a/src/auto-reply/reply/commands-plugins.ts +++ b/src/auto-reply/reply/commands-plugins.ts @@ -7,6 +7,7 @@ import { resolveFileNpmSpecToLocalPath, } from "../../cli/plugins-command-helpers.js"; import { persistPluginInstall } from "../../cli/plugins-install-persist.js"; +import { refreshPluginRegistryAfterConfigMutation } from "../../cli/plugins-registry-refresh.js"; import { readConfigFileSnapshot, validateConfigObjectWithPlugins, @@ -473,11 +474,23 @@ export const handlePluginsCommand: CommandHandler = async (params, allowTextComm }; } await writeConfigFile(validated.config); + let registryWarning: string | undefined; + await refreshPluginRegistryAfterConfigMutation({ + config: validated.config, + reason: "policy-changed", + logger: { + warn: (message) => { + registryWarning = message; + }, + }, + }); return { shouldContinue: false, reply: { - text: `🔌 Plugin "${plugin.id}" ${pluginsCommand.action}d in ${loaded.path}. Restart the gateway to apply.`, + text: + `🔌 Plugin "${plugin.id}" ${pluginsCommand.action}d in ${loaded.path}. Restart the gateway to apply.` + + (registryWarning ? `\n${registryWarning}` : ""), }, }; };