From a49803ec9077b56c6c7666ff4461361f0a4b7e32 Mon Sep 17 00:00:00 2001 From: mosir Date: Sat, 28 Feb 2026 11:26:22 +0800 Subject: [PATCH] fix(message): suppress duplicate sends within same round --- pkg/tools/message.go | 18 ++++++++++++++++++ pkg/tools/message_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/pkg/tools/message.go b/pkg/tools/message.go index 15ef4ff73..50335a88d 100644 --- a/pkg/tools/message.go +++ b/pkg/tools/message.go @@ -12,6 +12,9 @@ type MessageTool struct { defaultChannel string defaultChatID string sentInRound bool // Tracks whether a message was sent in the current processing round + lastChannel string + lastChatID string + lastContent string } func NewMessageTool() *MessageTool { @@ -51,6 +54,9 @@ func (t *MessageTool) SetContext(channel, chatID string) { t.defaultChannel = channel t.defaultChatID = chatID t.sentInRound = false // Reset send tracking for new processing round + t.lastChannel = "" + t.lastChatID = "" + t.lastContent = "" } // HasSentInRound returns true if the message tool sent a message during the current round. @@ -86,6 +92,15 @@ func (t *MessageTool) Execute(ctx context.Context, args map[string]any) *ToolRes return &ToolResult{ForLLM: "Message sending not configured", IsError: true} } + // Guard against accidental repeated tool-calls in one LLM round. + // If the same target and content were already sent, suppress duplicate delivery. + if t.sentInRound && t.lastChannel == channel && t.lastChatID == chatID && t.lastContent == content { + return &ToolResult{ + ForLLM: "Duplicate message suppressed in current round", + Silent: true, + } + } + if err := t.sendCallback(channel, chatID, content); err != nil { return &ToolResult{ ForLLM: fmt.Sprintf("sending message: %v", err), @@ -95,6 +110,9 @@ func (t *MessageTool) Execute(ctx context.Context, args map[string]any) *ToolRes } t.sentInRound = true + t.lastChannel = channel + t.lastChatID = chatID + t.lastContent = content // Silent: user already received the message directly return &ToolResult{ ForLLM: fmt.Sprintf("Message sent to %s:%s", channel, chatID), diff --git a/pkg/tools/message_test.go b/pkg/tools/message_test.go index 717c1117b..6cc643860 100644 --- a/pkg/tools/message_test.go +++ b/pkg/tools/message_test.go @@ -194,6 +194,38 @@ func TestMessageTool_Execute_NotConfigured(t *testing.T) { } } +func TestMessageTool_Execute_SuppressDuplicateInSameRound(t *testing.T) { + tool := NewMessageTool() + tool.SetContext("test-channel", "test-chat-id") + + callCount := 0 + tool.SetSendCallback(func(channel, chatID, content string) error { + callCount++ + return nil + }) + + ctx := context.Background() + args := map[string]any{ + "content": "same message", + } + + first := tool.Execute(ctx, args) + second := tool.Execute(ctx, args) + + if callCount != 1 { + t.Fatalf("send callback call count = %d, want 1", callCount) + } + if !first.Silent || first.IsError { + t.Fatalf("first result unexpected: %+v", first) + } + if !second.Silent || second.IsError { + t.Fatalf("second result unexpected: %+v", second) + } + if second.ForLLM != "Duplicate message suppressed in current round" { + t.Fatalf("second ForLLM = %q", second.ForLLM) + } +} + func TestMessageTool_Name(t *testing.T) { tool := NewMessageTool() if tool.Name() != "message" {