From 5355ef0f080bc79492ae7d55782b7e99e53aa539 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 13:51:14 -0700 Subject: [PATCH] fix(plugins): dedupe manifest diagnostics --- CHANGELOG.md | 1 + src/plugins/manifest-registry.test.ts | 30 +++++++++++++++++++++++++++ src/plugins/manifest-registry.ts | 16 +++++++++++++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 927276fd8e2..33483a87488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ Docs: https://docs.openclaw.ai - Doctor/Telegram: warn when selected Telegram quote replies can suppress `streaming.preview.toolProgress`, and document the `replyToMode` trade-off without changing runtime delivery. Fixes #73487. Thanks @GodsBoy. - Channels/Discord: send a best-effort native typing cue immediately after an inbound DM is accepted, so slow pre-dispatch turns show Discord liveness before queueing, context assembly, model, or tool work starts. Fixes #76417. Thanks @mlopez14. - Plugins/install: reject source-only TypeScript package installs and installed plugin packages that are missing compiled runtime output, so broken npm artifacts fail at install/discovery time instead of falling through jiti and surfacing later as unavailable providers. Fixes #76720. +- Plugins/config: deduplicate identical manifest compatibility diagnostics when an explicitly configured plugin overrides another discovered candidate, so external channel plugins do not print the same missing `channelConfigs` warning repeatedly during install and enable. Thanks @vincentkoc. - Discord/status: honor explicit `messages.statusReactions.enabled: true` in tool-only guild channels so queued ack reactions can progress through thinking/done lifecycle reactions instead of stopping at the initial emoji. Thanks @Marvinthebored. - Discord/native commands: compare Discord-normalized slash-command descriptions and localized descriptions during reconcile so CJK or multiline command text no longer triggers redundant startup PATCH bursts and rate-limit 429s. Fixes #76587. Thanks @zhengsx. - Agents/OpenAI: omit Chat Completions `reasoning_effort` for `gpt-5.4-mini` only when function tools are present while preserving tool-free Chat and Responses reasoning support, preventing Telegram-routed fallback runs from hanging after OpenAI rejects tool payloads. Fixes #76176. Thanks @ThisIsAdilah and @chinar-amrutkar. diff --git a/src/plugins/manifest-registry.test.ts b/src/plugins/manifest-registry.test.ts index 6053fca63fc..40a13f4fc3b 100644 --- a/src/plugins/manifest-registry.test.ts +++ b/src/plugins/manifest-registry.test.ts @@ -400,6 +400,36 @@ describe("loadPluginManifestRegistry", () => { expect(warning?.message).toContain(path.join(configDir, "index.ts")); }); + it("deduplicates compatibility diagnostics when a config plugin replaces a global candidate", () => { + const globalDir = makeTempDir(); + const configDir = makeTempDir(); + const manifest = { + id: "external-chat", + channels: ["external-chat"], + configSchema: { type: "object" }, + }; + writeManifest(globalDir, manifest); + writeManifest(configDir, manifest); + + const registry = loadRegistry([ + createPluginCandidate({ + idHint: "external-chat", + rootDir: globalDir, + origin: "global", + }), + createPluginCandidate({ + idHint: "external-chat", + rootDir: configDir, + origin: "config", + }), + ]); + + const channelConfigWarnings = registry.diagnostics.filter((diagnostic) => + diagnostic.message.includes("without channelConfigs metadata"), + ); + expect(channelConfigWarnings).toHaveLength(1); + }); + it("suppresses duplicate warnings for explicit installed globals overriding bundled plugins", () => { const bundledDir = makeTempDir(); const globalDir = makeTempDir(); diff --git a/src/plugins/manifest-registry.ts b/src/plugins/manifest-registry.ts index b0f05bef7c6..f77a2ed8434 100644 --- a/src/plugins/manifest-registry.ts +++ b/src/plugins/manifest-registry.ts @@ -490,6 +490,20 @@ function pushManifestCompatibilityDiagnostics(params: { pushNonBundledChannelConfigDescriptorDiagnostic(params); } +function dedupePluginDiagnostics(diagnostics: PluginDiagnostic[]): PluginDiagnostic[] { + const seen = new Set(); + const deduped: PluginDiagnostic[] = []; + for (const diagnostic of diagnostics) { + const key = JSON.stringify([diagnostic.level, diagnostic.pluginId ?? "", diagnostic.message]); + if (seen.has(key)) { + continue; + } + seen.add(key); + deduped.push(diagnostic); + } + return deduped; +} + function matchesInstalledPluginRecord(params: { pluginId: string; candidate: PluginCandidate; @@ -786,6 +800,6 @@ export function loadPluginManifestRegistry( pushManifestCompatibilityDiagnostics({ record, diagnostics }); } - const registry = { plugins: records, diagnostics }; + const registry = { plugins: records, diagnostics: dedupePluginDiagnostics(diagnostics) }; return registry; }