Merge pull request #2468 from NousResearch/hermes/hermes-5d6932ba

feat(discord): persistent typing indicator for DMs
This commit is contained in:
Teknium 2026-03-22 04:53:32 -07:00 committed by GitHub
commit 3037450c77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 68 additions and 8 deletions

View file

@ -504,6 +504,14 @@ class BasePlatformAdapter(ABC):
metadata: optional dict with platform-specific context (e.g. thread_id for Slack).
"""
pass
async def stop_typing(self, chat_id: str) -> None:
"""Stop a persistent typing indicator (if the platform uses one).
Override in subclasses that start background typing loops.
Default is a no-op for platforms with one-shot typing indicators.
"""
pass
async def send_image(
self,

View file

@ -439,6 +439,9 @@ class DiscordAdapter(BasePlatformAdapter):
# in those threads don't require @mention. Persisted to disk so the
# set survives gateway restarts.
self._bot_participated_threads: set = self._load_participated_threads()
# Persistent typing indicator loops per channel (DMs don't reliably
# show the standard typing gateway event for bots)
self._typing_tasks: Dict[str, asyncio.Task] = {}
# Cap to prevent unbounded growth (Discord threads get archived).
self._MAX_TRACKED_THREADS = 500
@ -1239,14 +1242,48 @@ class DiscordAdapter(BasePlatformAdapter):
return await super().send_document(chat_id, file_path, caption, file_name, reply_to, metadata=metadata)
async def send_typing(self, chat_id: str, metadata=None) -> None:
"""Send typing indicator."""
if self._client:
"""Start a persistent typing indicator for a channel.
Discord's TYPING_START gateway event is unreliable in DMs for bots.
Instead, start a background loop that hits the typing endpoint every
8 seconds (typing indicator lasts ~10s). The loop is cancelled when
stop_typing() is called (after the response is sent).
"""
if not self._client:
return
# Don't start a duplicate loop
if chat_id in self._typing_tasks:
return
async def _typing_loop() -> None:
try:
channel = self._client.get_channel(int(chat_id))
if channel:
await channel.typing()
except Exception:
pass # Ignore typing indicator failures
while True:
try:
route = discord.http.Route(
"POST", "/channels/{channel_id}/typing",
channel_id=chat_id,
)
await self._client.http.request(route)
except asyncio.CancelledError:
return
except Exception as e:
logger.debug("Discord typing indicator failed for %s: %s", chat_id, e)
return
await asyncio.sleep(8)
except asyncio.CancelledError:
pass
self._typing_tasks[chat_id] = asyncio.create_task(_typing_loop())
async def stop_typing(self, chat_id: str) -> None:
"""Stop the persistent typing indicator for a channel."""
task = self._typing_tasks.pop(chat_id, None)
if task:
task.cancel()
try:
await task
except (asyncio.CancelledError, Exception):
pass
async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
"""Get information about a Discord channel."""

View file

@ -2125,7 +2125,15 @@ class GatewayRunner:
session_id=session_entry.session_id,
session_key=session_key
)
# Stop persistent typing indicator now that the agent is done
try:
_typing_adapter = self.adapters.get(source.platform)
if _typing_adapter and hasattr(_typing_adapter, "stop_typing"):
await _typing_adapter.stop_typing(source.chat_id)
except Exception:
pass
response = agent_result.get("final_response") or ""
agent_messages = agent_result.get("messages", [])
@ -2327,6 +2335,13 @@ class GatewayRunner:
return response
except Exception as e:
# Stop typing indicator on error too
try:
_err_adapter = self.adapters.get(source.platform)
if _err_adapter and hasattr(_err_adapter, "stop_typing"):
await _err_adapter.stop_typing(source.chat_id)
except Exception:
pass
logger.exception("Agent error in session %s", session_key)
error_type = type(e).__name__
error_detail = str(e)[:300] if str(e) else "no details available"