fix(discord): slash registration and /skill refresh scan the catalog off the event loop

connect() called _register_slash_commands inline on every (re)connect, and
/reload-skills called refresh_skill_group inline; both run
discord_skill_commands_by_category, the same per-skill path-resolution walk
the Telegram menu paid in #110707. On a 1.5k-skill install that holds the
loop past the liveness watchdog. Registration now hops through
asyncio.to_thread from connect(); refresh_skill_group is a coroutine that
hops the rescan the same way (the reload handler already awaits an awaitable
result). Contextvar-scoped profile overrides propagate through to_thread.

One invariant test: the loop keeps ticking while the scan blocks, from both
sites. Red on the PR head, green here.

Review finding: Discord _register_slash_commands/refresh_skill_group ran the skill catalog disk scan synchronously on the event loop.
This commit is contained in:
teknium1
2026-09-14 21:08:09 -07:00
committed by Teknium
parent ab782a7685
commit 9c311038fa
3 changed files with 41 additions and 4 deletions
+4 -3
View File
@@ -1346,7 +1346,8 @@ class DiscordAdapter(DiscordMediaMixin, BasePlatformAdapter):
guild_id,
)
if self._slash_commands:
self._register_slash_commands()
# Registration walks the skill catalog on disk (#110707); keep the loop free.
await asyncio.to_thread(self._register_slash_commands)
self._disconnecting = False
self._bot_task = asyncio.create_task(self._client.start(self.config.token))
self._bot_task.add_done_callback(self._handle_bot_task_done)
@@ -4486,11 +4487,11 @@ class DiscordAdapter(DiscordMediaMixin, BasePlatformAdapter):
self._skill_lookup = {n: (d, k) for n, d, k in entries}
self._skill_group_hidden_count = hidden
def refresh_skill_group(self) -> tuple[int, int]:
async def refresh_skill_group(self) -> tuple[int, int]:
"""Rescan skills and refresh live ``/skill`` autocomplete; returns ``(new_count, hidden_count)``.
Called after ``reload_skills``; no ``tree.sync()`` since autocomplete options are dynamic."""
try:
self._refresh_skill_catalog_state()
await asyncio.to_thread(self._refresh_skill_catalog_state)
except Exception as exc:
logger.warning(
"[%s] Failed to refresh /skill autocomplete after reload: %s", self.name, exc,