fix: enforce priority order in Telegram menu — core > plugins > skills (#4023)

The menu now has explicit priority tiers:
1. Core CommandDef commands (always included, never bumped)
2. Plugin slash commands (take precedence over skills)
3. Built-in skill commands (fill remaining slots alphabetically)

Only skills get trimmed when the 100-command cap is hit. Adding new
core commands or plugin commands automatically pushes skills out,
not the other way around.
This commit is contained in:
Teknium
2026-03-30 13:04:06 -07:00
committed by GitHub
parent 86250a3e45
commit 158f49f19a

View File

@@ -366,27 +366,41 @@ def telegram_bot_commands() -> list[tuple[str, str]]:
def telegram_menu_commands(max_commands: int = 100) -> tuple[list[tuple[str, str]], int]:
"""Return Telegram menu commands (built-in + active skills), capped to the Bot API limit.
"""Return Telegram menu commands capped to the Bot API limit.
Built-in commands come first, then active skill commands. Commands beyond
``max_commands`` remain callable in the gateway; they are just omitted from
Telegram's native slash-command picker.
Priority order (higher priority = never bumped by overflow):
1. Core CommandDef commands (always included)
2. Plugin slash commands (take precedence over skills)
3. Built-in skill commands (fill remaining slots, alphabetical)
Skills are the only tier that gets trimmed when the cap is hit.
User-installed hub skills are excluded — accessible via /skills.
Returns:
(menu_commands, hidden_count) where hidden_count is the number of
commands omitted due to the cap.
skill commands omitted due to the cap.
"""
all_commands = list(telegram_bot_commands())
# Append active BUILT-IN skill commands only (not user-installed hub skills).
# User-installed skills stay accessible via /skills and by typing the command
# directly, but don't clutter the Telegram menu.
# Plugin slash commands get priority over skills
try:
from hermes_cli.plugins import get_plugin_manager
pm = get_plugin_manager()
plugin_cmds = getattr(pm, "_plugin_commands", {})
for cmd_name in sorted(plugin_cmds):
tg_name = cmd_name.replace("-", "_")
desc = "Plugin command"
if len(desc) > 40:
desc = desc[:37] + "..."
all_commands.append((tg_name, desc))
except Exception:
pass
# Remaining slots go to built-in skill commands (not hub-installed).
skill_entries: list[tuple[str, str]] = []
try:
from agent.skill_commands import get_skill_commands
from tools.skills_tool import SKILLS_DIR
# Built-in skills are synced to SKILLS_DIR (~/.hermes/skills/).
# Hub-installed skills go into SKILLS_DIR/.hub/. Exclude .hub/ skills
# from the menu — they're user-installed, not repo built-in.
_skills_dir = str(SKILLS_DIR.resolve())
_hub_dir = str((SKILLS_DIR / ".hub").resolve())
skill_cmds = get_skill_commands()
@@ -396,18 +410,21 @@ def telegram_menu_commands(max_commands: int = 100) -> tuple[list[tuple[str, str
if not skill_path.startswith(_skills_dir):
continue
if skill_path.startswith(_hub_dir):
continue # hub-installed, not built-in
continue
name = cmd_key.lstrip("/").replace("-", "_")
desc = info.get("description", "")
# Keep descriptions short — setMyCommands has an undocumented
# total payload limit. 40 chars fits 100 commands safely.
if len(desc) > 40:
desc = desc[:37] + "..."
all_commands.append((name, desc))
skill_entries.append((name, desc))
except Exception:
pass
hidden_count = max(0, len(all_commands) - max_commands)
# Skills fill remaining slots — they're the only tier that gets trimmed
remaining_slots = max(0, max_commands - len(all_commands))
hidden_count = max(0, len(skill_entries) - remaining_slots)
all_commands.extend(skill_entries[:remaining_slots])
return all_commands[:max_commands], hidden_count