fix(telegram): last draft push is the final text so the client folds it into the message

This commit is contained in:
hh
2026-08-28 18:51:16 +02:00
parent 7240e0cdac
commit 43407016d0
3 changed files with 36 additions and 11 deletions
@@ -1,5 +1,9 @@
"""One ``sendMessageDraft`` stream per running turn (§3.8).
The client folds a draft into the message that follows only when their
texts are identical - so the last push is the final text itself, rendered
exactly as the outbox will send it, with no status line.
A draft is ephemeral and lives 30 s, Telegram throttles edits to about one
per second per chat, and thinking or a tool call would otherwise look like a
hang - so the draft opens with a status line straight away, is refreshed on
@@ -74,15 +78,29 @@ class Draft:
The final ``sendMessage`` must reach Telegram after the last draft,
or the draft lands on top of the message and lingers for its 30 s.
"""
if self._task is None:
return
self._task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await self._task
self._task = None
if self._task is not None:
self._task.cancel()
await asyncio.gather(self._task, return_exceptions=True)
self._task = None
if self._inflight is not None:
with contextlib.suppress(Exception):
await self._inflight
self._inflight = None
async def finish(self, text: str) -> None:
"""Last push: the final text verbatim, so the message replaces the draft."""
await self.stop()
if self._broken or not text.strip():
return
try:
try:
await self._send(to_html(text), "HTML")
except TelegramBadRequest:
await self._send(text, None)
except TelegramAPIError as exc:
_log.warning(
"final draft to %s/%s failed: %s", self._chat_id, self._thread_id, exc
)
async def _run(self) -> None:
while not self._broken:
@@ -39,7 +39,7 @@ from beaver_gateway.frontends.base import Frontend
from beaver_gateway.frontends.telegram.drafts import Draft
from beaver_gateway.frontends.telegram.inbox import Inbox
from beaver_gateway.frontends.telegram.outbox import Outbox
from beaver_gateway.frontends.telegram.render import status_label
from beaver_gateway.frontends.telegram.render import chunks, status_label
if TYPE_CHECKING:
from beaver_gateway.core.bus import Event, EventBus
@@ -659,7 +659,9 @@ class TelegramFrontend(Frontend):
f"{status_label(str(event['name']), event.get('input'))}"
)
case "turn.end":
await self._close_draft(key)
draft = self._drafts.get(key)
if draft is not None:
await draft.stop()
if event.get("stop") == "error" and event.get("origin") == "user":
await self._deliver(
conv,
@@ -691,7 +693,11 @@ class TelegramFrontend(Frontend):
async def _on_reply(self, conv: Conversation, event: Event) -> None:
turn_id = str(event.get("turn_id") or "")
origin = str(event.get("item_origin") or "")
text = str(event.get("text") or "")
await self._unreact(event.get("item"))
draft = self._drafts.pop(conv.external_id, None)
if draft is not None:
await draft.finish(chunks(text)[-1] if text.strip() else "")
if origin != FRONTEND and not origin.startswith("сид"):
user_text = str(event.get("user_text") or "")
if user_text:
@@ -701,9 +707,7 @@ class TelegramFrontend(Frontend):
turn_id=turn_id,
key=f"{turn_id}:mirror",
)
await self._deliver(
conv, str(event.get("text") or ""), turn_id=turn_id, key=f"{turn_id}:reply"
)
await self._deliver(conv, text, turn_id=turn_id, key=f"{turn_id}:reply")
# ---- drafts ------------------------------------------------------------