Skip to content

[Bug]: Telegram /approve and /deny Commands Silently Drop During Active Agent Runs #4898

@mechovation

Description

@mechovation

Bug Description

Problem Statement

When the agent attempts to execute a dangerous command via the Telegram gateway:

  1. Approval request IS delivered — user sees "⚠️ Dangerous command requires approval..."
  2. User replies /approve
  3. Result: total silence. The bot gives no response. The agent thread sits blocked until it times out.

This works correctly in the terminal CLI. It is broken specifically on Telegram (and likely all messaging platforms that route through the gateway's handle_message path).


Investigation Summary

Theories checked and ruled out

Theory Status Reason
HOME directory move (/opt/data/home) Ruled out Gateway starts fine, sessions/DB/config all load
Config not found at $HERMES_HOME/config.yaml Ruled out HERMES_HOME=/opt/data, config at /opt/data/config.yaml — confirmed
Telegram adapter not running Ruled out Gateway lock present, messages flow both ways
get_hermes_home() resolving wrong Ruled out Returns /opt/data, state.db exists at /opt/data/state.db
Approval notification not being sent Ruled out User confirmed receipt of the approval prompt
Command not recognized as /approve Ruled out Telegram parses /approve as MessageType.COMMAND; get_command() strips @botname
Session key mismatch (registration vs resolution) Ruled out Both paths use _session_key_for_source(source) — keys matched

Steps to Reproduce

  1. create test file in home: delete_me.txt
  2. ask agent from Telegram to delete file
  3. submit /approve

Expected Behavior

File should be deleted and agent resumes

Actual Behavior

agent waits for time out because approval is blocked

Affected Component

Gateway (Telegram/Discord/Slack/WhatsApp)

Messaging Platform (if gateway-related)

Telegram

Operating System

Ubuntu 24.04 server

Python Version

Python: 3.13.5

Hermes Version

Hermes Agent v0.7.0 (2026.4.3)

Relevant Logs / Traceback

Root Cause Analysis (optional)

Root Cause

Active-session guard in gateway/platforms/base.py intercepts /approve and /deny commands and queues them instead of dispatching them, creating a deadlock.

The code path

handle_message in gateway/platforms/base.py (line ~1023):

async def handle_message(self, event: MessageEvent) -> None:
    session_key = build_session_key(event.source, ...)

    if session_key in self._active_sessions:
        # Session is busy — queue message and signal interrupt
        self._pending_messages[session_key] = event     # ← /approve lands HERE
        self._active_sessions[session_key].set()
        return  # ← exits without dispatching

When the agent is running and hits a dangerous command:

  1. Agent thread blocks on a synchronous threading.Event inside tools/approval.py
  2. Approval notification is sent to the user via Telegram
  3. User replies /approve
  4. Telegram's _handle_command builds a MessageType.COMMAND event and calls handle_message(event)
  5. handle_message sees session_key in self._active_sessions is true
  6. The /approve event is queued in _pending_messages and never dispatched
  7. The agent thread waits forever for approval that was silently queued
  8. The pending queued message won't be processed until the agent run completes
  9. The agent run can't complete because it's waiting for the approval

Classic deadlock. The agent waits for approval. The approval waits for the agent to finish.

Why CLI works

The terminal CLI uses synchronous input() via prompt_toolkit in the same process. No active-session guard, no message routing, no session key matching. The user's response goes directly to the blocking thread.

Why messaging platforms break

All gateway platforms (Telegram, Discord, WhatsApp, etc.) route messages through the base adapter's handle_message, which has the active-session guard. The guard treats /approve and /deny the same as any other message — it queues them. These control commands need to reach the gateway runner's command routing (run.py line 1828) to call resolve_gateway_approval() and unblock the agent thread.

Proposed Fix (optional)

Fix

File: gateway/platforms/base.py

Location: handle_message method, inside the active-session guard (line ~1023)

Change: Add command bypass for /approve and /deny

Before:

        # Check if there's already an active handler for this session
        if session_key in self._active_sessions:
            # Special case: photo bursts/albums frequently arrive as multiple near-
            # simultaneous messages. Queue them without interrupting the active run,
            # then process them immediately after the current task finishes.
            if event.message_type == MessageType.PHOTO:

After:

        # Check if there's already an active handler for this session
        if session_key in self._active_sessions:
            # Gateway-level commands (/approve, /deny) must ALWAYS reach the
            # gateway runner so they can resolve synchronous approval blockages.
            # The agent thread is blocked on a threading.Event inside
            # tools/approval.py — a soft interrupt won't unblock it. These
            # commands bypass the active-session guard and are dispatched
            # immediately. Without this, /approve gets queued silently and
            # the agent times out waiting for user input that never arrives.
            cmd = event.get_command()
            if cmd in ("approve", "deny"):
                logger.debug(
                    "[%s] Gateway command '%s' bypassing active-session guard for %s",
                    self.name, cmd, session_key,
                )
                task = asyncio.create_task(
                    self._process_message_background(event, session_key)
                )
                try:
                    self._background_tasks.add(task)
                except TypeError:
                    return
                if hasattr(task, "add_done_callback"):
                    task.add_done_callback(self._background_tasks.discard)
                return
            # Special case: photo bursts/albums frequently arrive as multiple near-
            # simultaneous messages. Queue them without interrupting the active run,
            # then process them immediately after the current task finishes.
            if event.message_type == MessageType.PHOTO:

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions