From 6a2949de4a82b6688cd2bbd60e24de59a88329de Mon Sep 17 00:00:00 2001 From: viettranx Date: Tue, 24 Mar 2026 07:51:13 +0700 Subject: [PATCH] fix(telegram): clear draft after materializing DM stream When using draft_transport in Telegram DMs, the preview draft sometimes stays in the input area after the real message is delivered, showing a brief duplicate reply. Clear the stale draft by sending an empty sendMessageDraft as soon as the final message is sent in Send(). The cleanup is asynchronous and best-effort so it won't block message delivery. Co-authored-by: Badgerbees --- internal/channels/telegram/channel.go | 1 + internal/channels/telegram/send.go | 18 ++++++++++++++++++ internal/channels/telegram/stream.go | 5 +++++ 3 files changed, 24 insertions(+) diff --git a/internal/channels/telegram/channel.go b/internal/channels/telegram/channel.go index 16c3dd13..d97f6d13 100644 --- a/internal/channels/telegram/channel.go +++ b/internal/channels/telegram/channel.go @@ -46,6 +46,7 @@ type Channel struct { pollDone chan struct{} // closed when polling goroutine exits handlerWg sync.WaitGroup // tracks in-flight handler goroutines for graceful shutdown handlerSem chan struct{} // bounded semaphore for concurrent handler goroutines + pendingDraftID sync.Map // localKey string → int (draftID) } type thinkingCancel struct { diff --git a/internal/channels/telegram/send.go b/internal/channels/telegram/send.go index e0754df6..39c9a941 100644 --- a/internal/channels/telegram/send.go +++ b/internal/channels/telegram/send.go @@ -161,6 +161,24 @@ func (c *Channel) Send(ctx context.Context, msg bus.OutboundMessage) error { } } + // Captured draft cleanup: if this message follows a DM stream that used the + // draft transport, we must clear the stale draft as soon as this bubble is sent. + if pID, ok := c.pendingDraftID.LoadAndDelete(localKey); ok { + draftID := pID.(int) + go func() { + params := &telego.SendMessageDraftParams{ + ChatID: chatID, + DraftID: draftID, + Text: "", + MessageThreadID: resolveThreadIDForSend(threadID), + } + // Best-effort with Background ctx — caller ctx may already be cancelled. + if err := c.bot.SendMessageDraft(context.Background(), params); err != nil { + slog.Debug("telegram: draft clear failed (cosmetic)", "chat_id", chatID, "draft_id", draftID, "error", err) + } + }() + } + // Placeholder update (e.g. LLM retry notification): edit the placeholder // but keep it alive for the final response. Don't stop typing or cleanup. if msg.Metadata["placeholder_update"] == "true" { diff --git a/internal/channels/telegram/stream.go b/internal/channels/telegram/stream.go index e65691bc..6070b57d 100644 --- a/internal/channels/telegram/stream.go +++ b/internal/channels/telegram/stream.go @@ -331,6 +331,11 @@ func (c *Channel) FinalizeStream(ctx context.Context, chatID string, stream chan slog.Warn("stream: initial send landed but ID unknown. Suppressing fallback message to avoid duplicate.", "chat_id", chatID) } + // Capture draft ID for clearing after the final Send() + if ds, ok := stream.(*DraftStream); ok && ds.UsedDraftTransport() { + c.pendingDraftID.Store(chatID, ds.draftID) + } + // Stop thinking animation if stop, ok := c.stopThinking.Load(chatID); ok { if cf, ok := stop.(*thinkingCancel); ok {