From ddcc7a1a5d27161bd547b58a03dc484609d17000 Mon Sep 17 00:00:00 2001 From: seewhy Date: Mon, 16 Feb 2026 10:33:51 +0800 Subject: [PATCH] fix(discord): dedupe native skill commands by skillName (#17365) * fix(discord): dedupe native skill commands by skill name * Changelog: credit Discord skill dedupe --------- Co-authored-by: yume Co-authored-by: Shadow --- CHANGELOG.md | 1 + .../monitor/provider.skill-dedupe.test.ts | 26 +++++++++++++++++++ src/discord/monitor/provider.ts | 25 +++++++++++++++++- 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 src/discord/monitor/provider.skill-dedupe.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e326d61aa9..09a0b109143 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai - Security/Logging: redact Telegram bot tokens from error messages and uncaught stack traces to prevent accidental secret leakage into logs. Thanks @aether-ai-agent. - Telegram: omit `message_thread_id` for DM sends/draft previews and keep forum-topic handling (`id=1` general omitted, non-general kept), preventing DM failures with `400 Bad Request: message thread not found`. (#10942) Thanks @garnetlyx. - Dev tooling: harden git `pre-commit` hook against option injection from malicious filenames (for example `--force`), preventing accidental staging of ignored files. Thanks @mrthankyou. +- Discord: dedupe native skill commands by skill name in multi-agent setups to prevent duplicated slash commands with `_2` suffixes. (#17365) Thanks @seewhyme. - Subagents/Models: preserve `agents.defaults.model.fallbacks` when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model. - Config/Gateway: make sensitive-key whitelist suffix matching case-insensitive while preserving `passwordFile` path exemptions, preventing accidental redaction of non-secret config values like `maxTokens` and IRC password-file paths. (#16042) Thanks @akramcodez. - Group chats: always inject group chat context (name, participants, reply guidance) into the system prompt on every turn, not just the first. Prevents the model from losing awareness of which group it's in and incorrectly using the message tool to send to the same group. (#14447) Thanks @tyler6204. diff --git a/src/discord/monitor/provider.skill-dedupe.test.ts b/src/discord/monitor/provider.skill-dedupe.test.ts new file mode 100644 index 00000000000..06e27774010 --- /dev/null +++ b/src/discord/monitor/provider.skill-dedupe.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { __testing } from "./provider.js"; + +describe("dedupeSkillCommandsForDiscord", () => { + it("keeps first command per skillName and drops suffix duplicates", () => { + const input = [ + { name: "github", skillName: "github", description: "GitHub" }, + { name: "github_2", skillName: "github", description: "GitHub" }, + { name: "weather", skillName: "weather", description: "Weather" }, + { name: "weather_2", skillName: "weather", description: "Weather" }, + ]; + + const output = __testing.dedupeSkillCommandsForDiscord(input); + expect(output.map((entry) => entry.name)).toEqual(["github", "weather"]); + }); + + it("treats skillName case-insensitively", () => { + const input = [ + { name: "ClawHub", skillName: "ClawHub", description: "ClawHub" }, + { name: "clawhub_2", skillName: "clawhub", description: "ClawHub" }, + ]; + const output = __testing.dedupeSkillCommandsForDiscord(input); + expect(output).toHaveLength(1); + expect(output[0]?.name).toBe("ClawHub"); + }); +}); diff --git a/src/discord/monitor/provider.ts b/src/discord/monitor/provider.ts index 2996eb18c98..55040ce10a8 100644 --- a/src/discord/monitor/provider.ts +++ b/src/discord/monitor/provider.ts @@ -81,6 +81,26 @@ function summarizeGuilds(entries?: Record) { return `${sample.join(", ")}${suffix}`; } +function dedupeSkillCommandsForDiscord( + skillCommands: ReturnType, +) { + const seen = new Set(); + const deduped: ReturnType = []; + for (const command of skillCommands) { + const key = command.skillName.trim().toLowerCase(); + if (!key) { + deduped.push(command); + continue; + } + if (seen.has(key)) { + continue; + } + seen.add(key); + deduped.push(command); + } + return deduped; +} + async function deployDiscordCommands(params: { client: Client; runtime: RuntimeEnv; @@ -355,7 +375,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { const maxDiscordCommands = 100; let skillCommands = - nativeEnabled && nativeSkillsEnabled ? listSkillCommandsForAgents({ cfg }) : []; + nativeEnabled && nativeSkillsEnabled + ? dedupeSkillCommandsForDiscord(listSkillCommandsForAgents({ cfg })) + : []; let commandSpecs = nativeEnabled ? listNativeCommandSpecsForConfig(cfg, { skillCommands, provider: "discord" }) : []; @@ -648,4 +670,5 @@ async function clearDiscordNativeCommands(params: { export const __testing = { createDiscordGatewayPlugin, + dedupeSkillCommandsForDiscord, };