fix(discord): cap slash commands at Discord's 100-command limit

Discord enforces a hard cap of 100 global application commands per app.
The adapter registers ~27 native commands plus every gateway-available
entry in COMMAND_REGISTRY plus all plugin commands plus the consolidated
/skill group. On a loaded install (many plugins/quick commands) the
desired set exceeds 100, so tree.sync() / _safe_sync_slash_commands()
hits error 30032 ("Maximum number of application commands reached") and
Discord rejects the ENTIRE batch — silently breaking every slash command,
not just the overflow.

Cap registration at the 100-command limit: native commands (registered
first, highest priority) and the /skill group are always kept; lower-
priority auto-registered COMMAND_REGISTRY and plugin commands are added
only until the cap is reached, with a single concise warning telling the
user how to surface the rest. Since both sync paths read from
tree.get_commands(), bounding the tree fixes the root cause for both.
This commit is contained in:
xxxigm 2026-06-14 17:01:28 +07:00 committed by CodeForgeNet
parent e4f98d2323
commit 1f407057a8

View file

@ -31,6 +31,12 @@ _DISCORD_COMMAND_SYNC_STATE_SUBDIR = "gateway"
_DISCORD_COMMAND_SYNC_STATE_FILENAME = "discord_command_sync_state.json"
_DISCORD_COMMAND_SYNC_MUTATION_INTERVAL_SECONDS = 4.5
_DISCORD_COMMAND_SYNC_MAX_RATE_LIMIT_SLEEP_SECONDS = 30.0
# Discord enforces a hard cap of 100 global application (slash) commands per
# app. Registering more makes the ENTIRE sync fail with error 30032
# ("Maximum number of application commands reached"), which silently breaks
# every slash command — not just the overflow ones. We keep the desired set
# at or below this limit at registration time.
_DISCORD_MAX_APP_COMMANDS = 100
try:
import discord
@ -3518,6 +3524,11 @@ class DiscordAdapter(BasePlatformAdapter):
)
already_registered: set[str] = set()
# Native commands above are registered first and are the highest
# priority, so they always survive the 100-command cap. Reserve one
# slot for the consolidated ``/skill`` group registered further below.
slot_cap = _DISCORD_MAX_APP_COMMANDS - 1
dropped_over_cap = 0
try:
from hermes_cli.commands import COMMAND_REGISTRY, _is_gateway_available, _resolve_config_gates
@ -3535,6 +3546,9 @@ class DiscordAdapter(BasePlatformAdapter):
discord_name = cmd_def.name.lower()[:32]
if discord_name in already_registered:
continue
if len(already_registered) >= slot_cap:
dropped_over_cap += 1
continue
auto_cmd = _build_auto_slash_command(
cmd_def.name,
cmd_def.description,
@ -3567,6 +3581,9 @@ class DiscordAdapter(BasePlatformAdapter):
discord_name = plugin_name.lower()[:32]
if discord_name in already_registered:
continue
if len(already_registered) >= slot_cap:
dropped_over_cap += 1
continue
auto_cmd = _build_auto_slash_command(
plugin_name,
plugin_desc,
@ -3589,6 +3606,20 @@ class DiscordAdapter(BasePlatformAdapter):
# supporting up to 25 categories × 25 skills = 625 skills.
self._register_skill_group(tree)
if dropped_over_cap:
# Staying under the cap keeps the whole sync succeeding; without
# this guard a single over-limit command makes Discord reject the
# entire batch (error 30032), breaking every slash command.
logger.warning(
"[%s] Reached Discord's limit of %d slash commands; skipped %d "
"lower-priority command(s) to keep the command sync working. "
"Disable slash commands you don't need or trim installed plugins "
"to surface them all.",
self.name,
_DISCORD_MAX_APP_COMMANDS,
dropped_over_cap,
)
# Optional defense-in-depth: hide every slash command from non-admin
# guild members in Discord's slash picker. Server-side authorization
# (``_check_slash_authorization``) is the actual gate; this is purely