From c8684b2996de01ff3f31ba0a61bd7e1027978bb7 Mon Sep 17 00:00:00 2001 From: EloquentBrush0x Date: Sat, 19 Sep 2026 03:59:18 +0300 Subject: [PATCH] fix(telegram): send_clarify budgets the HTML-escaped rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit send_clarify escaped the question and each choice individually with no total length budget, unlike the exec-approval and slash-confirm cards fixed earlier today (b502504b74, edd9fd66a4). A long, model-controlled question or many/long choices can expand past Telegram's 4096-char cap once HTML-escaped, and Telegram answers "Message is too long" instead of sending the prompt at all — the same failure mode those two cards were just fixed for. Reuses the shared _ea_fit bisection (gateway/platforms/base.py) the same way send_slash_confirm does, including reserving budget for both the header and _ea_fit's own "..." truncation suffix (which rides outside the budget it's given, per its docstring) — an oversight caught by the new tests before this was accounted for. Co-Authored-By: Claude Sonnet 5 --- plugins/platforms/telegram/adapter.py | 15 ++++++- .../gateway/test_telegram_clarify_buttons.py | 45 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/plugins/platforms/telegram/adapter.py b/plugins/platforms/telegram/adapter.py index 4d1c7f0d9e0e6..bdfadf3f98f97 100644 --- a/plugins/platforms/telegram/adapter.py +++ b/plugins/platforms/telegram/adapter.py @@ -4058,15 +4058,26 @@ async def send_clarify( """Render a clarify prompt: numbered buttons per choice plus "✏️ Other (type answer)" (flips to text-capture mode); without choices, plain question and the gateway text-intercept captures.""" def build(): - text = f"❓ {_html.escape(question)}" + header = "❓ " + body = str(question) keyboard = None if choices: # Full option text in the body (mobile truncates button labels); buttons keep numeric labels. - text += "\n\n" + "\n".join(f"{i + 1}. {_html.escape(str(c))}" for i, c in enumerate(choices)) + body += "\n\n" + "\n".join(f"{i + 1}. {c}" for i, c in enumerate(choices)) # Telegram caps callback_data at 64 bytes; keep "cl::" short. rows = [[InlineKeyboardButton(str(idx + 1), callback_data=f"cl:{clarify_id}:{idx}")] for idx in range(len(choices))] rows.append([InlineKeyboardButton("✏️ Other (type answer)", callback_data=f"cl:{clarify_id}:other")]) keyboard = InlineKeyboardMarkup(rows) + # Budget the HTML-escaped rendering (escaping expands text), same as the exec-approval + # and slash-confirm cards — an unbudgeted question/choice set can exceed the 4096 cap + # and Telegram answers "Message is too long" instead of sending the prompt at all. + # ``_ea_fit``'s "..." suffix rides outside the budget it's given (by design, like + # ``_truncate_preview``), so the header AND that suffix are both reserved up front — + # same margin ``send_slash_confirm`` reserves for its own "..." above. + budget = ( + self.MAX_MESSAGE_LENGTH - utf16_len(self._ea_escape(header)) + - utf16_len(self._ea_escape("..."))) + text = header + self._ea_escape(self._ea_fit(body, budget, escape=self._ea_escape)) return text, keyboard, lambda msg: self._clarify_state.__setitem__(clarify_id, session_key) return await self._send_prompt( "send_clarify", chat_id, metadata, build, parse_mode=ParseMode.HTML, thread_id=self._metadata_thread_id(metadata)) diff --git a/tests/gateway/test_telegram_clarify_buttons.py b/tests/gateway/test_telegram_clarify_buttons.py index 0b1ec990e2c75..5c9c2117fac7d 100644 --- a/tests/gateway/test_telegram_clarify_buttons.py +++ b/tests/gateway/test_telegram_clarify_buttons.py @@ -112,6 +112,51 @@ async def test_html_escapes_question(self): assert "