mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-17 09:41:58 +00:00
Merge pull request #46078 from xxxigm/fix/discord-slash-command-100-cap
fix(discord): cap slash commands at Discord's 100-command limit
This commit is contained in:
commit
cffd6e3c8d
2 changed files with 83 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -292,6 +292,58 @@ async def test_plugin_command_name_conflict_skipped(adapter):
|
|||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 100-command cap (Discord error 30032 guard)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_slash_command_registration_stays_under_discord_limit(adapter):
|
||||
"""Registering far more commands than Discord allows must NOT push the
|
||||
tree over the 100-command hard cap.
|
||||
|
||||
Discord rejects the ENTIRE command sync with error 30032 once the
|
||||
desired set exceeds 100 global application commands, silently breaking
|
||||
every slash command. The adapter must bound the desired set instead.
|
||||
Regression guard for samuraiheart's recurring
|
||||
"Maximum number of application commands reached (100)" sync failures.
|
||||
"""
|
||||
from plugins.platforms.discord.adapter import _DISCORD_MAX_APP_COMMANDS
|
||||
|
||||
adapter._run_simple_slash = AsyncMock()
|
||||
|
||||
# 200 plugin commands — way past Discord's limit on their own.
|
||||
many_plugins = {
|
||||
f"plug{i:03d}": {
|
||||
"handler": lambda _a: "ok",
|
||||
"description": f"Plugin command {i}",
|
||||
"args_hint": "",
|
||||
"plugin": "stress-plugin",
|
||||
}
|
||||
for i in range(200)
|
||||
}
|
||||
|
||||
with patch("hermes_cli.plugins.get_plugin_commands", return_value=many_plugins):
|
||||
adapter._register_slash_commands()
|
||||
|
||||
tree_names = set(adapter._client.tree.commands.keys())
|
||||
|
||||
# Contract: never exceed Discord's hard cap.
|
||||
assert len(tree_names) <= _DISCORD_MAX_APP_COMMANDS, (
|
||||
f"registered {len(tree_names)} commands — exceeds Discord's "
|
||||
f"{_DISCORD_MAX_APP_COMMANDS} limit and would fail sync with 30032"
|
||||
)
|
||||
|
||||
# Native, high-priority commands are registered first and must survive
|
||||
# the cap — they are the core UX, not droppable overflow.
|
||||
for native in ("status", "stop", "new", "model", "help"):
|
||||
assert native in tree_names, f"/{native} (native) was dropped by the cap"
|
||||
|
||||
# The cap must actually have dropped overflow — not every plugin fit.
|
||||
registered_plugins = [n for n in tree_names if n.startswith("plug")]
|
||||
assert len(registered_plugins) < 200, "cap did not drop any overflow commands"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# _handle_thread_create_slash — success, session dispatch, failure
|
||||
# ------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue