diff --git a/internal/channels/events.go b/internal/channels/events.go index e96b722d..25ef84be 100644 --- a/internal/channels/events.go +++ b/internal/channels/events.go @@ -70,8 +70,6 @@ func (m *Manager) HandleAgentEvent(eventType, runID string, payload any) { // Stop the current stream (reasoning or answer) and finalize only // the answer stream (reasoning messages stay visible). rc.mu.Lock() - // Capture current state before resetting for next iteration. - wasReasoningStream := rc.hasThinking && !rc.thinkingDone currentStream := rc.stream rc.stream = nil rc.inToolPhase = true @@ -84,17 +82,18 @@ func (m *Manager) HandleAgentEvent(eventType, runID string, payload any) { if err := currentStream.Stop(ctx); err != nil { slog.Debug("stream tool-phase stop failed", "channel", rc.ChannelName, "error", err) } - // Only finalize answer streams (hand off messageID to Send()). - // Reasoning streams stay as visible messages — don't put their - // messageID into placeholders or it would confuse Send(). - if !wasReasoningStream { - sc.FinalizeStream(ctx, rc.ChatID, currentStream) - } + // Don't finalize mid-run streams — their messageID must NOT go + // into placeholders. Otherwise tool_status placeholder_update + // overwrites streamed content, and subsequent FinalizeStream + // calls overwrite the placeholder key, leaving earlier messages + // stuck at tool status text. Only run.completed finalizes. } - // Show tool status in streaming preview (edit placeholder with tool name). + // Show tool status by editing placeholder message (non-streaming only). + // Streaming channels show tool status via reaction emoji instead — + // editing the placeholder would overwrite streamed content. toolName := extractPayloadString(payload, "name") - if toolName != "" && rc.ToolStatusEnabled { + if toolName != "" && rc.ToolStatusEnabled && !rc.Streaming { statusText := formatToolStatus(toolName) outMeta := copyRoutingMeta(rc.Metadata) outMeta["placeholder_update"] = "true"