hermes - 💡(How to fix) Fix Clarify tool breaks in gateway/Telegram mode — missing callback wiring

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

After _status_callback_sync block, before run_sync():

def _clarify_callback(question: str, choices=None) -> str: if not _status_adapter or not _run_still_current(): return "" import uuid, threading clarify_id = uuid.uuid4().hex event = threading.Event() _status_adapter._clarify_state[clarify_id] = { "event": event, "choice": None, "choices": choices or [], "question": question, } try: asyncio.run_coroutine_threadsafe( _status_adapter.send_clarify_prompt( chat_id=_status_chat_id, question=question, choices=choices or [], clarify_id=clarify_id, metadata=_status_thread_metadata, ), _loop_for_step, ).result(timeout=10) except Exception as _e: logger.error("clarify_callback send error: %s", _e) _status_adapter._clarify_state.pop(clarify_id, None) return "" logger.info("clarify pending stored: %s choices=%s", clarify_id, choices) event.wait(timeout=120) state = _status_adapter._clarify_state.pop(clarify_id, {"choice": ""}) return state.get("choice", "")

After agent.status_callback = _status_callback_sync:

agent.clarify_callback = _clarify_callback

Root Cause

AIAgent requires clarify_callback to wire the tool to the platform. In CLI (cli.py) and TUI (tui_gateway/server.py) this callback is passed during agent construction. In the messaging gateway (gateway/run.py) it is never passed — self.clarify_callback remains None, which clarify_tool.py:57 treats as unavailable.

Additionally, the Telegram adapter (gateway/platforms/telegram.py) has no:

  • _clarify_state dict for storing prompt state
  • send_clarify_prompt() method
  • clarify: handler in _handle_callback_query

Fix Action

Fix

2 files changed, ~145 insertions. Full diff below.

Code Example

{"error": "Clarify tool is not available in this execution context."}

---

2026-05-07 07:01:37 gateway.run: clarify pending stored: 16ab104a... choices=[...]
2026-05-07 07:01:46 gateway.platforms.telegram: clarify callback received: cq:16ab104a...:0
2026-05-07 07:01:46 gateway.platforms.telegram: clarify resolved: 16ab104a... idx=0

---

# After _status_callback_sync block, before run_sync():
def _clarify_callback(question: str, choices=None) -> str:
    if not _status_adapter or not _run_still_current():
        return ""
    import uuid, threading
    clarify_id = uuid.uuid4().hex
    event = threading.Event()
    _status_adapter._clarify_state[clarify_id] = {
        "event": event, "choice": None,
        "choices": choices or [], "question": question,
    }
    try:
        asyncio.run_coroutine_threadsafe(
            _status_adapter.send_clarify_prompt(
                chat_id=_status_chat_id,
                question=question, choices=choices or [],
                clarify_id=clarify_id,
                metadata=_status_thread_metadata,
            ), _loop_for_step,
        ).result(timeout=10)
    except Exception as _e:
        logger.error("clarify_callback send error: %s", _e)
        _status_adapter._clarify_state.pop(clarify_id, None)
        return ""
    logger.info("clarify pending stored: %s choices=%s", clarify_id, choices)
    event.wait(timeout=120)
    state = _status_adapter._clarify_state.pop(clarify_id, {"choice": ""})
    return state.get("choice", "")

# After agent.status_callback = _status_callback_sync:
agent.clarify_callback = _clarify_callback

---

self._clarify_state: Dict[str, dict] = {}

---

async def send_clarify_prompt(
    self, chat_id: str, question: str, choices: list,
    clarify_id: str, metadata=None,
) -> SendResult:
    if not self._bot:
        return SendResult(success=False, error="Not connected")
    try:
        buttons = []
        for i, choice in enumerate(choices):
            buttons.append([InlineKeyboardButton(
                choice, callback_data=f"clarify:{clarify_id}:{i}")])
        buttons.append([InlineKeyboardButton(
            "✗ Skip", callback_data=f"clarify:{clarify_id}:skip")])
        keyboard = InlineKeyboardMarkup(buttons)
        thread_id = metadata.get("thread_id") if metadata else None
        msg = await self._bot.send_message(
            chat_id=int(chat_id), text=question,
            reply_markup=keyboard,
            message_thread_id=int(thread_id) if thread_id else None,
            **self._link_preview_kwargs(),
        )
        return SendResult(success=True, message_id=str(msg.message_id))
    except Exception as e:
        logger.warning("[%s] send_clarify_prompt failed: %s", self.name, e)
        return SendResult(success=False, error=str(e))

---

# --- Clarify callbacks (clarify:id:idx) ---
if data.startswith("clarify:"):
    parts = data.split(":", 2)
    if len(parts) == 3:
        clarify_id = parts[1]
        choice_key = parts[2]
        state = self._clarify_state.get(clarify_id)
        if not state:
            await query.answer(text="This question is no longer active.")
            return
        if choice_key == "skip":
            user_choice = ""
        else:
            try:
                idx = int(choice_key)
            except ValueError:
                await query.answer(text="Invalid choice.")
                return
            choices = state.get("choices", [])
            if 0 <= idx < len(choices):
                user_choice = choices[idx]
            else:
                await query.answer(text="Invalid choice.")
                return
        state["choice"] = user_choice
        state["event"].set()
        user_display = getattr(query.from_user, "first_name", "User")
        label = f"Selected: {user_choice}" if user_choice else "Skipped"
        await query.answer(text=label)
        try:
            await query.edit_message_text(
                text=f"{state['question']}\n\n*{label}* by {user_display}",
                parse_mode=ParseMode.MARKDOWN, reply_markup=None,
            )
        except Exception:
            pass
        logger.info(
            "[%s] clarify resolved: %s idx=%s choice=%r",
            self.name, clarify_id, choice_key, user_choice,
        )
    return
RAW_BUFFERClick to expand / collapse

Bug Description

After upgrading Hermes to current HEAD, the clarify tool always returns an error in Telegram gateway mode:

{"error": "Clarify tool is not available in this execution context."}

Inline keyboard buttons never appear. The tool works correctly in CLI and TUI modes — the bug is specific to gateway/Telegram.

Steps to Reproduce

  1. Start Hermes gateway with Telegram platform: hermes gateway start
  2. Send any message that causes the agent to call the clarify tool
  3. Observe: no inline keyboard appears in Telegram
  4. The agent receives {"error": "Clarify tool is not available in this execution context."} and cannot ask the user questions

Reproduces every time — the tool is completely non-functional in gateway mode.

Expected Behavior

The agent sends a Telegram message with inline keyboard buttons showing the choices. User taps a button → choice is returned to the agent.

Actual Behavior

Error returned immediately. No message sent to Telegram. No buttons.

Environment

  • Hermes Agent: v0.12.0 (2026.4.30), updated to HEAD (691 commits ahead of tag)
  • Platform: Telegram gateway (polling mode)
  • Python: 3.11.15
  • OS: Linux (systemd user service)
  • Gateway started fresh after upgrade

Evidence

Before upgrade (gateway started April 27) — clarify worked:

2026-05-07 07:01:37 gateway.run: clarify pending stored: 16ab104a... choices=[...]
2026-05-07 07:01:46 gateway.platforms.telegram: clarify callback received: cq:16ab104a...:0
2026-05-07 07:01:46 gateway.platforms.telegram: clarify resolved: 16ab104a... idx=0

After upgrade + restart — clarify never fires. No log entries for clarify at all. The tool returns the error above.

Root Cause

AIAgent requires clarify_callback to wire the tool to the platform. In CLI (cli.py) and TUI (tui_gateway/server.py) this callback is passed during agent construction. In the messaging gateway (gateway/run.py) it is never passed — self.clarify_callback remains None, which clarify_tool.py:57 treats as unavailable.

Additionally, the Telegram adapter (gateway/platforms/telegram.py) has no:

  • _clarify_state dict for storing prompt state
  • send_clarify_prompt() method
  • clarify: handler in _handle_callback_query

Impact

Every gateway user on Telegram (and likely other messaging platforms that do not implement their own clarify integration) cannot use the clarify tool. The agent silently fails to ask clarifying questions.

Fix

2 files changed, ~145 insertions. Full diff below.

gateway/run.py — callback + wiring

# After _status_callback_sync block, before run_sync():
def _clarify_callback(question: str, choices=None) -> str:
    if not _status_adapter or not _run_still_current():
        return ""
    import uuid, threading
    clarify_id = uuid.uuid4().hex
    event = threading.Event()
    _status_adapter._clarify_state[clarify_id] = {
        "event": event, "choice": None,
        "choices": choices or [], "question": question,
    }
    try:
        asyncio.run_coroutine_threadsafe(
            _status_adapter.send_clarify_prompt(
                chat_id=_status_chat_id,
                question=question, choices=choices or [],
                clarify_id=clarify_id,
                metadata=_status_thread_metadata,
            ), _loop_for_step,
        ).result(timeout=10)
    except Exception as _e:
        logger.error("clarify_callback send error: %s", _e)
        _status_adapter._clarify_state.pop(clarify_id, None)
        return ""
    logger.info("clarify pending stored: %s choices=%s", clarify_id, choices)
    event.wait(timeout=120)
    state = _status_adapter._clarify_state.pop(clarify_id, {"choice": ""})
    return state.get("choice", "")

# After agent.status_callback = _status_callback_sync:
agent.clarify_callback = _clarify_callback

gateway/platforms/telegram.py — adapter support

1. In __init__:

self._clarify_state: Dict[str, dict] = {}

2. New method send_clarify_prompt:

async def send_clarify_prompt(
    self, chat_id: str, question: str, choices: list,
    clarify_id: str, metadata=None,
) -> SendResult:
    if not self._bot:
        return SendResult(success=False, error="Not connected")
    try:
        buttons = []
        for i, choice in enumerate(choices):
            buttons.append([InlineKeyboardButton(
                choice, callback_data=f"clarify:{clarify_id}:{i}")])
        buttons.append([InlineKeyboardButton(
            "✗ Skip", callback_data=f"clarify:{clarify_id}:skip")])
        keyboard = InlineKeyboardMarkup(buttons)
        thread_id = metadata.get("thread_id") if metadata else None
        msg = await self._bot.send_message(
            chat_id=int(chat_id), text=question,
            reply_markup=keyboard,
            message_thread_id=int(thread_id) if thread_id else None,
            **self._link_preview_kwargs(),
        )
        return SendResult(success=True, message_id=str(msg.message_id))
    except Exception as e:
        logger.warning("[%s] send_clarify_prompt failed: %s", self.name, e)
        return SendResult(success=False, error=str(e))

3. In _handle_callback_query, before update-prompt handler:

# --- Clarify callbacks (clarify:id:idx) ---
if data.startswith("clarify:"):
    parts = data.split(":", 2)
    if len(parts) == 3:
        clarify_id = parts[1]
        choice_key = parts[2]
        state = self._clarify_state.get(clarify_id)
        if not state:
            await query.answer(text="This question is no longer active.")
            return
        if choice_key == "skip":
            user_choice = ""
        else:
            try:
                idx = int(choice_key)
            except ValueError:
                await query.answer(text="Invalid choice.")
                return
            choices = state.get("choices", [])
            if 0 <= idx < len(choices):
                user_choice = choices[idx]
            else:
                await query.answer(text="Invalid choice.")
                return
        state["choice"] = user_choice
        state["event"].set()
        user_display = getattr(query.from_user, "first_name", "User")
        label = f"Selected: {user_choice}" if user_choice else "Skipped"
        await query.answer(text=label)
        try:
            await query.edit_message_text(
                text=f"{state['question']}\n\n*{label}* by {user_display}",
                parse_mode=ParseMode.MARKDOWN, reply_markup=None,
            )
        except Exception:
            pass
        logger.info(
            "[%s] clarify resolved: %s idx=%s choice=%r",
            self.name, clarify_id, choice_key, user_choice,
        )
    return

Notes

  • Uses uuid.uuid4().hex for clarify IDs (no colons) — avoids the split(":", 2) pitfall documented in references/clarify-telegram-buttons.md
  • Follows the existing callback pattern from _status_callback_sync
  • Fix has been tested in production — clarify tool works correctly after applying this patch

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix Clarify tool breaks in gateway/Telegram mode — missing callback wiring