From 610e9e3fe8c012ff2a66dba657697b446e759d51 Mon Sep 17 00:00:00 2001 From: Anton Bogdanovich <27antonb@gmail.com> Date: Thu, 7 May 2026 21:06:18 -0700 Subject: [PATCH 1/2] fix(agent): dismiss session tool feedback on skipped outbound --- pkg/agent/agent_outbound.go | 10 +++++++ pkg/agent/agent_test.go | 57 +++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/pkg/agent/agent_outbound.go b/pkg/agent/agent_outbound.go index 1728f6f79..f4a01adfd 100644 --- a/pkg/agent/agent_outbound.go +++ b/pkg/agent/agent_outbound.go @@ -56,6 +56,16 @@ func (al *AgentLoop) PublishResponseIfNeeded(ctx context.Context, channel, chatI } if alreadySentToSameChat { + if al.channelManager != nil && channel != "" && chatID != "" { + dismissCtx, dismissCancel := context.WithTimeout(ctx, 5*time.Second) + al.channelManager.DismissToolFeedback( + dismissCtx, + channel, + chatID, + nil, + ) + dismissCancel() + } logger.DebugCF( "agent", "Skipped outbound (message tool already sent to same chat)", diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go index a75919912..cf693930d 100644 --- a/pkg/agent/agent_test.go +++ b/pkg/agent/agent_test.go @@ -57,6 +57,28 @@ func (f *fakeMediaChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaM return nil, nil } +type recordingChannelManager struct { + dismissed []string +} + +func (m *recordingChannelManager) GetChannel(name string) (channels.Channel, bool) { return nil, false } +func (m *recordingChannelManager) GetEnabledChannels() []string { return nil } +func (m *recordingChannelManager) InvokeTypingStop(channel, chatID string) {} +func (m *recordingChannelManager) SendMessage(ctx context.Context, msg bus.OutboundMessage) error { + return nil +} +func (m *recordingChannelManager) SendMedia(ctx context.Context, msg bus.OutboundMediaMessage) error { + return nil +} +func (m *recordingChannelManager) SendPlaceholder(ctx context.Context, channel, chatID string) bool { + return false +} +func (m *recordingChannelManager) DismissToolFeedback( + ctx context.Context, channel, chatID string, outboundCtx *bus.InboundContext, +) { + m.dismissed = append(m.dismissed, fmt.Sprintf("%s:%s", channel, chatID)) +} + func newStartedTestChannelManager( t *testing.T, msgBus *bus.MessageBus, @@ -214,6 +236,41 @@ func TestNewAgentLoop_DoesNotRegisterWebSearchTool_WhenNoReadyProviders(t *testi } } +func TestPublishResponseIfNeeded_DismissesToolFeedbackWhenMessageToolAlreadySent(t *testing.T) { + al, _, _, _, cleanup := newTestAgentLoop(t) + defer cleanup() + + cm := &recordingChannelManager{} + al.channelManager = cm + + defaultAgent := al.registry.GetDefaultAgent() + if defaultAgent == nil { + t.Fatal("expected default agent") + } + mt := tools.NewMessageTool() + mt.SetSendCallback(func(ctx context.Context, channel, chatID, content, replyToMessageID string) error { + return nil + }) + defaultAgent.Tools.Register(mt) + + result := mt.Execute( + tools.WithToolSessionContext(context.Background(), "main", "session-1", nil), + map[string]any{ + "content": "ack", + "channel": "telegram", + "chat_id": "-100123", + }, + ) + if result == nil || result.IsError { + t.Fatalf("message tool execute failed: %+v", result) + } + al.PublishResponseIfNeeded(context.Background(), "telegram", "-100123", "session-1", "final reply") + + if got := cm.dismissed; len(got) != 1 || got[0] != "telegram:-100123" { + t.Fatalf("dismissed = %v, want [telegram:-100123]", got) + } +} + func TestProcessMessage_IncludesCurrentSenderInDynamicContext(t *testing.T) { tmpDir, err := os.MkdirTemp("", "agent-test-*") if err != nil { From a3edbcd05e37ba7c55fba2a81b104cd22f5afabb Mon Sep 17 00:00:00 2001 From: Anton Bogdanovich <27antonb@gmail.com> Date: Fri, 8 May 2026 10:33:16 -0700 Subject: [PATCH 2/2] test(agent): satisfy lint for tool feedback cleanup --- pkg/agent/agent_test.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go index cf693930d..7a869ec94 100644 --- a/pkg/agent/agent_test.go +++ b/pkg/agent/agent_test.go @@ -61,18 +61,28 @@ type recordingChannelManager struct { dismissed []string } -func (m *recordingChannelManager) GetChannel(name string) (channels.Channel, bool) { return nil, false } -func (m *recordingChannelManager) GetEnabledChannels() []string { return nil } -func (m *recordingChannelManager) InvokeTypingStop(channel, chatID string) {} +func (m *recordingChannelManager) GetChannel(name string) (channels.Channel, bool) { + return nil, false +} + +func (m *recordingChannelManager) GetEnabledChannels() []string { + return nil +} + +func (m *recordingChannelManager) InvokeTypingStop(channel, chatID string) {} + func (m *recordingChannelManager) SendMessage(ctx context.Context, msg bus.OutboundMessage) error { return nil } + func (m *recordingChannelManager) SendMedia(ctx context.Context, msg bus.OutboundMediaMessage) error { return nil } + func (m *recordingChannelManager) SendPlaceholder(ctx context.Context, channel, chatID string) bool { return false } + func (m *recordingChannelManager) DismissToolFeedback( ctx context.Context, channel, chatID string, outboundCtx *bus.InboundContext, ) { @@ -237,8 +247,11 @@ func TestNewAgentLoop_DoesNotRegisterWebSearchTool_WhenNoReadyProviders(t *testi } func TestPublishResponseIfNeeded_DismissesToolFeedbackWhenMessageToolAlreadySent(t *testing.T) { - al, _, _, _, cleanup := newTestAgentLoop(t) + al, msgBus, provider, sessions, cleanup := newTestAgentLoop(t) defer cleanup() + _ = msgBus + _ = provider + _ = sessions cm := &recordingChannelManager{} al.channelManager = cm