From 89e7a61a69da517c254f50be46dd3e5387b2e84c Mon Sep 17 00:00:00 2001 From: Andy Lo-A-Foe Date: Wed, 27 May 2026 20:36:23 +0200 Subject: [PATCH] fix(channels): prevent tool_calls from being dropped during streaming The auxiliary message filtering introduced in #2892 incorrectly drops tool_calls messages when there is an active stream or a tombstone from a recently finished stream. This causes tool_calls to not be delivered to the UI when users make consecutive requests, as the second request's tool_calls message arrives while the first request's stream is still active or within the 30-second tombstone window. The fix excludes tool_calls from auxiliary message filtering, since they represent new tool invocations that must be delivered to the UI, not stale auxiliary content like feedback or thoughts. Co-Authored-By: Claude Opus 4.5 --- pkg/channels/manager.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go index 03f65e11a..c50b19994 100644 --- a/pkg/channels/manager.go +++ b/pkg/channels/manager.go @@ -169,6 +169,13 @@ func outboundMessageIsToolFeedback(msg bus.OutboundMessage) bool { return strings.EqualFold(strings.TrimSpace(msg.Context.Raw["message_kind"]), "tool_feedback") } +func outboundMessageIsToolCalls(msg bus.OutboundMessage) bool { + if len(msg.Context.Raw) == 0 { + return false + } + return strings.EqualFold(strings.TrimSpace(msg.Context.Raw["message_kind"]), "tool_calls") +} + func outboundMessageHasAuxiliaryKind(msg bus.OutboundMessage) bool { if len(msg.Context.Raw) == 0 { return false @@ -379,6 +386,7 @@ func (m *Manager) preSend(ctx context.Context, name string, msg bus.OutboundMess } isToolFeedback := outboundMessageIsToolFeedback(msg) + isToolCalls := outboundMessageIsToolCalls(msg) isAuxiliaryMessage := outboundMessageHasAuxiliaryKind(msg) isFinalMessage := outboundMessageIsFinal(msg) separateToolFeedbackMessages := m.toolFeedbackSeparateMessagesEnabled() @@ -388,7 +396,9 @@ func (m *Manager) preSend(ctx context.Context, name string, msg bus.OutboundMess // finalization bypasses the worker queue, so older queued feedback/thoughts // can arrive before the normal final outbound message that cleans up the // marker and placeholder. - if isAuxiliaryMessage { + // Note: tool_calls messages must NOT be dropped as they represent new tool + // invocations for the current turn that must be delivered to the UI. + if isAuxiliaryMessage && !isToolCalls { if _, loaded := m.streamActive.Load(streamKey); loaded { return nil, true }