From 5db008f3841bab4158069aa0b339f41db319583a Mon Sep 17 00:00:00 2001 From: Guoguo <16666742+imguoguo@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:17:55 +0800 Subject: [PATCH] fix(channels): dismiss tool feedback animation when turn ends via ResponseHandled (#2713) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(channels): dismiss tool feedback animation when turn ends via ResponseHandled When a tool sets ResponseHandled=true (e.g., send_file), the turn ends without producing a final assistant response. This meant no outbound message triggered FinalizeToolFeedbackMessage, leaving the animation goroutine running indefinitely — editing the Feishu card every 3 seconds with "." / ".." suffixes long after the tool had finished. Fix: call DismissToolFeedback at "Tool output satisfied delivery" so the tracker is cleared and the animation goroutine is stopped immediately. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(adapters): add DismissToolFeedback to channelManagerAdapter The adapter must implement the new interface method added in the previous commit, otherwise the package fails to compile. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(channels): pass InboundContext to DismissToolFeedback for topic-aware keys Telegram forum topics use scoped tracker keys like "chatID/topicID", resolved via ToolFeedbackMessageChatID with the InboundContext. The previous nil context caused the lookup to fall back to the raw chatID, missing the topic-scoped entry and leaving the animation goroutine orphaned in forum-topic conversations. Co-Authored-By: Claude Opus 4.7 (1M context) * style: wrap long function signatures for golines Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- pkg/agent/adapters/channelmanager.go | 6 ++++++ pkg/agent/interfaces/interfaces.go | 7 +++++++ pkg/agent/pipeline_execute.go | 3 +++ pkg/channels/manager.go | 15 +++++++++++++++ 4 files changed, 31 insertions(+) diff --git a/pkg/agent/adapters/channelmanager.go b/pkg/agent/adapters/channelmanager.go index 8265ef99d..ad0840e86 100644 --- a/pkg/agent/adapters/channelmanager.go +++ b/pkg/agent/adapters/channelmanager.go @@ -43,3 +43,9 @@ func (a *channelManagerAdapter) SendMedia(ctx context.Context, msg bus.OutboundM func (a *channelManagerAdapter) SendPlaceholder(ctx context.Context, channel, chatID string) bool { return a.inner.SendPlaceholder(ctx, channel, chatID) } + +func (a *channelManagerAdapter) DismissToolFeedback( + ctx context.Context, channel, chatID string, outboundCtx *bus.InboundContext, +) { + a.inner.DismissToolFeedback(ctx, channel, chatID, outboundCtx) +} diff --git a/pkg/agent/interfaces/interfaces.go b/pkg/agent/interfaces/interfaces.go index bdf483e20..2efec05e1 100644 --- a/pkg/agent/interfaces/interfaces.go +++ b/pkg/agent/interfaces/interfaces.go @@ -44,4 +44,11 @@ type ChannelManager interface { // SendPlaceholder sends a placeholder message (e.g., for audio transcription). SendPlaceholder(ctx context.Context, channel, chatID string) bool + + // DismissToolFeedback clears any tracked tool feedback animation for the + // given channel/chat. Call this when a turn ends without a final response + // (e.g., ResponseHandled tools) to avoid orphaned animation goroutines. + // outboundCtx carries topic/thread info needed for channels that use + // scoped tracker keys (e.g., Telegram forum topics); may be nil. + DismissToolFeedback(ctx context.Context, channel, chatID string, outboundCtx *bus.InboundContext) } diff --git a/pkg/agent/pipeline_execute.go b/pkg/agent/pipeline_execute.go index 9935f2c9e..f6a8eaad6 100644 --- a/pkg/agent/pipeline_execute.go +++ b/pkg/agent/pipeline_execute.go @@ -704,6 +704,9 @@ toolLoop: } ts.setPhase(TurnPhaseCompleted) ts.setFinalContent("") + if al.channelManager != nil && ts.channel != "" { + al.channelManager.DismissToolFeedback(ctx, ts.channel, ts.chatID, ts.opts.InboundContext) + } logger.InfoCF("agent", "Tool output satisfied delivery; ending turn without follow-up LLM", map[string]any{ "agent_id": ts.agent.ID, diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go index d56c4fd9b..c6dcfebe3 100644 --- a/pkg/channels/manager.go +++ b/pkg/channels/manager.go @@ -192,6 +192,21 @@ func clearTrackedToolFeedbackMessage( } } +// DismissToolFeedback clears any tracked tool feedback animation for the +// given channel/chat. This is called when a turn ends without a final +// response (e.g., ResponseHandled tools) to stop orphaned animation goroutines. +// outboundCtx carries topic/thread info for channels that use scoped tracker +// keys (e.g., Telegram forum topics); may be nil for non-topic channels. +func (m *Manager) DismissToolFeedback( + ctx context.Context, channelName, chatID string, outboundCtx *bus.InboundContext, +) { + ch, ok := m.GetChannel(channelName) + if !ok { + return + } + dismissTrackedToolFeedbackMessage(ctx, ch, chatID, outboundCtx) +} + func prepareToolFeedbackMessageContent(ch Channel, content string) string { prepared := strings.TrimSpace(content) if prepared == "" {