What
Found during a code-reduction audit of the Telegram message-turn pipeline in telegram/bot.py. Three related bugs where the error message shown to the user can misrepresent what actually happened.
- A successful agent turn followed by a failed send shows a generic, misleading error.
_run_turn_and_send_response (bot.py:1143) calls _run_user_turn_with_retry then _send_final_response. If the turn succeeds (the agent produced and committed a real response to the ADK session) but _send_final_response subsequently raises a non-parse-failure TelegramApiError or network error, the caller's outer except Exception in _handle_message/handle_scheduled_reminder/_handle_file_upload reports a generic "Sorry, I encountered an error" — the user never sees the real answer and has no way to recover it short of /reset or re-asking, even though the agent's turn was not wasted.
- Partial multi-chunk send followed by a later chunk failure. In
_send_final_response (bot.py:1280), if send_message succeeds for chunk N but a later chunk raises a non-parse TelegramApiError, the function re-raises after already sending some chunks — the caller's generic exception handler then also sends "Sorry, I encountered an error," producing a confusing partial-real-answer-plus-spurious-error-message sequence in the chat.
- A failure inside
rewind_empty_model_response masks the more accurate error message. In _run_user_turn_with_retry (bot.py:1076, rewind call at line 1124): if rewind_empty_model_response itself throws (e.g. a transient runner/session error) while recovering from an EmptyModelResponseError, that new exception propagates without being caught by any except EmptyModelResponseError clause upstream, so _handle_message's dedicated "please resend, the model returned an empty response" message never fires — the user gets the generic error text instead, which is less actionable.
Priority
Medium — item 1 is the most user-visible: it can make a real, already-computed answer effectively disappear, with the only recovery being /reset or re-asking. Items 2 and 3 are lower-frequency edge cases (transient network/API failures) but same root cause: outer generic exception handlers can't distinguish "the turn failed" from "the turn succeeded but sending/bookkeeping failed."
Level of Effort
Medium (M) — the fix likely means _run_turn_and_send_response (or its callers) distinguishing exceptions raised during the turn itself from exceptions raised while sending the already-computed response, and giving the latter a distinct, more accurate error message (e.g. "I have a response but couldn't deliver it, try /model or wait a moment" vs. the current generic text). Item 2 additionally needs a decision on whether to suppress the generic error when at least one chunk was already delivered.
Sources
Passing criteria / definition of done
- A test simulates a successful
_run_user_turn_with_retry followed by a _send_final_response failure, and asserts the user-facing error text is distinct from (and more accurate than) the generic "encountered an error processing your message" text — or that the response is retried/recovered instead of lost.
- A test simulates a multi-chunk response where an early chunk sends successfully and a later chunk raises, and asserts the user does not receive both a partial real answer and a generic failure message with no indication that part of the answer already arrived.
- A test simulates
rewind_empty_model_response raising during retry recovery and asserts the resulting user-facing message still communicates "please resend" rather than the fully generic error text (or documents why the generic text is acceptable here).
pytest tests/test_telegram_bot.py -q passes with the new assertions.
ruff check, ruff format --check, and mypy src/blacki/telegram/ all pass with no new warnings.
What
Found during a code-reduction audit of the Telegram message-turn pipeline in
telegram/bot.py. Three related bugs where the error message shown to the user can misrepresent what actually happened._run_turn_and_send_response(bot.py:1143) calls_run_user_turn_with_retrythen_send_final_response. If the turn succeeds (the agent produced and committed a real response to the ADK session) but_send_final_responsesubsequently raises a non-parse-failureTelegramApiErroror network error, the caller's outerexcept Exceptionin_handle_message/handle_scheduled_reminder/_handle_file_uploadreports a generic "Sorry, I encountered an error" — the user never sees the real answer and has no way to recover it short of/resetor re-asking, even though the agent's turn was not wasted._send_final_response(bot.py:1280), ifsend_messagesucceeds for chunk N but a later chunk raises a non-parseTelegramApiError, the function re-raises after already sending some chunks — the caller's generic exception handler then also sends "Sorry, I encountered an error," producing a confusing partial-real-answer-plus-spurious-error-message sequence in the chat.rewind_empty_model_responsemasks the more accurate error message. In_run_user_turn_with_retry(bot.py:1076, rewind call at line 1124): ifrewind_empty_model_responseitself throws (e.g. a transient runner/session error) while recovering from anEmptyModelResponseError, that new exception propagates without being caught by anyexcept EmptyModelResponseErrorclause upstream, so_handle_message's dedicated "please resend, the model returned an empty response" message never fires — the user gets the generic error text instead, which is less actionable.Priority
Medium — item 1 is the most user-visible: it can make a real, already-computed answer effectively disappear, with the only recovery being
/resetor re-asking. Items 2 and 3 are lower-frequency edge cases (transient network/API failures) but same root cause: outer generic exception handlers can't distinguish "the turn failed" from "the turn succeeded but sending/bookkeeping failed."Level of Effort
Medium (M) — the fix likely means
_run_turn_and_send_response(or its callers) distinguishing exceptions raised during the turn itself from exceptions raised while sending the already-computed response, and giving the latter a distinct, more accurate error message (e.g. "I have a response but couldn't deliver it, try /model or wait a moment" vs. the current generic text). Item 2 additionally needs a decision on whether to suppress the generic error when at least one chunk was already delivered.Sources
telegram/bot.py_run_turn_and_send_responsetelegram/bot.py_send_final_responsetelegram/bot.py_run_user_turn_with_retrytests/test_telegram_bot.py(existing coverage to extend)Passing criteria / definition of done
_run_user_turn_with_retryfollowed by a_send_final_responsefailure, and asserts the user-facing error text is distinct from (and more accurate than) the generic "encountered an error processing your message" text — or that the response is retried/recovered instead of lost.rewind_empty_model_responseraising during retry recovery and asserts the resulting user-facing message still communicates "please resend" rather than the fully generic error text (or documents why the generic text is acceptable here).pytest tests/test_telegram_bot.py -qpasses with the new assertions.ruff check,ruff format --check, andmypy src/blacki/telegram/all pass with no new warnings.