fix(telegram): prioritize dynamic skill menu commands

This commit is contained in:
LOGIN-TB
2026-08-09 14:25:24 +02:00
committed by Teknium
parent 83f4524b42
commit 60a664519c
2 changed files with 106 additions and 10 deletions
+42 -10
View File
@@ -836,6 +836,8 @@ def _telegram_command_menu_config() -> dict[str, Any]:
raw_priority = menu_cfg.get("priority")
if isinstance(raw_priority, list):
priority = [str(item) for item in raw_priority if str(item).strip()]
elif isinstance(raw_priority, str) and raw_priority.strip():
priority = [raw_priority]
else:
priority = []
@@ -1091,6 +1093,32 @@ def _collect_gateway_skill_entries(
# any clamp-induced renames.
skill_triples = _clamp_command_names(skill_triples, reserved_names)
# Telegram's configured command-menu priority applies to dynamic skills as
# well as core commands. Reorder before trimming so a prioritized skill can
# claim a scarce remaining BotCommand slot instead of losing to the
# alphabetical default order.
if platform == "telegram":
priority = {
name: index
for index, name in enumerate(_telegram_effective_priority())
}
skill_triples = [
entry
for original_index, entry in sorted(
enumerate(skill_triples),
key=lambda item: (
0,
priority[item[1][0]],
item[0],
)
if item[1][0] in priority
else (
1,
item[0],
),
)
]
# Skills fill remaining slots — only tier that gets trimmed
remaining = max(0, max_slots - len(all_entries))
hidden_count = max(0, len(skill_triples) - remaining)
@@ -1112,7 +1140,12 @@ def telegram_menu_commands(max_commands: int = 100) -> tuple[list[tuple[str, str
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.
Core, plugin, and skill tiers keep their existing relative order unless a
command is named in ``platforms.telegram.extra.command_menu.priority``.
Explicit priority is applied to the combined candidate list before the Bot
API cap, so a prioritized dynamic command can displace an unprioritized core
command when the core tier already fills the menu.
User-installed hub skills are excluded — accessible via /skills.
Skills disabled for the ``"telegram"`` platform (via ``hermes skills
config``) are excluded from the menu entirely.
@@ -1121,22 +1154,21 @@ def telegram_menu_commands(max_commands: int = 100) -> tuple[list[tuple[str, str
(menu_commands, hidden_count) where hidden_count is the number of
commands omitted due to the cap.
"""
core_commands = _prioritize_telegram_menu_commands(list(telegram_bot_commands()))
core_commands = list(telegram_bot_commands())
reserved_names = {n for n, _ in core_commands}
all_commands = list(core_commands)
hidden_core_count = max(0, len(all_commands) - max_commands)
remaining_slots = max(0, max_commands - len(all_commands))
entries, hidden_count = _collect_gateway_skill_entries(
platform="telegram",
max_slots=remaining_slots,
max_slots=max_commands,
reserved_names=reserved_names,
desc_limit=40,
sanitize_name=_sanitize_telegram_name,
)
# Drop the cmd_key — Telegram only needs (name, desc) pairs.
all_commands.extend((n, d) for n, d, _k in entries)
return all_commands[:max_commands], hidden_count + hidden_core_count
# Drop the cmd_key — Telegram only needs (name, desc) pairs. Apply the
# configured priority across all tiers before enforcing the global cap.
all_commands = core_commands + [(n, d) for n, d, _k in entries]
all_commands = _prioritize_telegram_menu_commands(all_commands)
overflow_count = max(0, len(all_commands) - max_commands)
return all_commands[:max_commands], hidden_count + overflow_count
def discord_skill_commands(
+64
View File
@@ -807,6 +807,70 @@ class TestTelegramMenuCommands:
# No empty string in menu names
assert "" not in menu_names
def test_configured_priority_promotes_skill_into_last_menu_slot(
self, tmp_path, monkeypatch
):
"""A prioritized dynamic skill must not be trimmed behind alphabetical peers."""
from unittest.mock import patch
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
local_dir = tmp_path / "skills"
local_dir.mkdir()
fake_cmds = {
"/aaa-skill": {
"name": "aaa-skill",
"description": "Alphabetically first",
"skill_md_path": f"{local_dir}/aaa-skill/SKILL.md",
"skill_dir": f"{local_dir}/aaa-skill",
},
"/gym": {
"name": "gym",
"description": "GymPilot",
"skill_md_path": f"{local_dir}/gym/SKILL.md",
"skill_dir": f"{local_dir}/gym",
},
}
core_count = len(telegram_bot_commands())
with (
patch("agent.skill_commands.get_skill_commands", return_value=fake_cmds),
patch("tools.skills_tool.SKILLS_DIR", local_dir),
patch(
"hermes_cli.commands._telegram_effective_priority",
return_value=("gym",),
),
):
menu, hidden = telegram_menu_commands(max_commands=core_count)
menu_names = [name for name, _description in menu]
assert len(menu_names) == core_count
assert menu_names[0] == "gym"
assert "aaa_skill" not in menu_names
assert hidden == 2
def test_scalar_configured_priority_is_accepted_as_one_command(self):
"""The config CLI's scalar value form must work for a single priority."""
from unittest.mock import patch
from hermes_cli.commands import _telegram_effective_priority
raw_config = {
"platforms": {
"telegram": {
"extra": {
"command_menu": {
"priority": "gym",
"priority_mode": "prepend",
}
}
}
}
}
with patch("hermes_cli.config.read_raw_config", return_value=raw_config):
priority = _telegram_effective_priority()
assert priority[0] == "gym"
# ---------------------------------------------------------------------------
# Backward-compat aliases