fix: improve message handling and bus closure logic for better reliability

This commit is contained in:
tong3jie 2026-03-13 17:17:27 +08:00
parent 41dff3b9b9
commit 334c08a27f
3 changed files with 70 additions and 61 deletions

View file

@ -251,63 +251,65 @@ func (al *AgentLoop) Run(ctx context.Context) error {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil return nil
case msg := <-al.bus.InboundChan(): case msg, ok := <-al.bus.InboundChan():
if !ok {
return nil
}
// Process message // Process message
func() { // TODO: Re-enable media cleanup after inbound media is properly consumed by the agent.
// TODO: Re-enable media cleanup after inbound media is properly consumed by the agent. // Currently disabled because files are deleted before the LLM can access their content.
// Currently disabled because files are deleted before the LLM can access their content. // defer func() {
// defer func() { // if al.mediaStore != nil && msg.MediaScope != "" {
// if al.mediaStore != nil && msg.MediaScope != "" { // if releaseErr := al.mediaStore.ReleaseAll(msg.MediaScope); releaseErr != nil {
// if releaseErr := al.mediaStore.ReleaseAll(msg.MediaScope); releaseErr != nil { // logger.WarnCF("agent", "Failed to release media", map[string]any{
// logger.WarnCF("agent", "Failed to release media", map[string]any{ // "scope": msg.MediaScope,
// "scope": msg.MediaScope, // "error": releaseErr.Error(),
// "error": releaseErr.Error(), // })
// }) // }
// } // }
// } // }()
// }()
response, err := al.processMessage(ctx, msg) response, err := al.processMessage(ctx, msg)
if err != nil { if err != nil {
response = fmt.Sprintf("Error processing message: %v", err) response = fmt.Sprintf("Error processing message: %v", err)
} }
if response != "" { if response != "" {
// Check if the message tool already sent a response during this round. // Check if the message tool already sent a response during this round.
// If so, skip publishing to avoid duplicate messages to the user. // If so, skip publishing to avoid duplicate messages to the user.
// Use default agent's tools to check (message tool is shared). // Use default agent's tools to check (message tool is shared).
alreadySent := false alreadySent := false
defaultAgent := al.GetRegistry().GetDefaultAgent() defaultAgent := al.GetRegistry().GetDefaultAgent()
if defaultAgent != nil { if defaultAgent != nil {
if tool, ok := defaultAgent.Tools.Get("message"); ok { if tool, ok := defaultAgent.Tools.Get("message"); ok {
if mt, ok := tool.(*tools.MessageTool); ok { if mt, ok := tool.(*tools.MessageTool); ok {
alreadySent = mt.HasSentInRound() alreadySent = mt.HasSentInRound()
}
} }
} }
if !alreadySent {
al.bus.PublishOutbound(ctx, bus.OutboundMessage{
Channel: msg.Channel,
ChatID: msg.ChatID,
Content: response,
})
logger.InfoCF("agent", "Published outbound response",
map[string]any{
"channel": msg.Channel,
"chat_id": msg.ChatID,
"content_len": len(response),
})
} else {
logger.DebugCF(
"agent",
"Skipped outbound (message tool already sent)",
map[string]any{"channel": msg.Channel},
)
}
} }
}()
if !alreadySent {
al.bus.PublishOutbound(ctx, bus.OutboundMessage{
Channel: msg.Channel,
ChatID: msg.ChatID,
Content: response,
})
logger.InfoCF("agent", "Published outbound response",
map[string]any{
"channel": msg.Channel,
"chat_id": msg.ChatID,
"content_len": len(response),
})
} else {
logger.DebugCF(
"agent",
"Skipped outbound (message tool already sent)",
map[string]any{"channel": msg.Channel},
)
}
}
default: default:
time.Sleep(time.Second * 5)
} }
} }

View file

@ -88,11 +88,13 @@ func (mb *MessageBus) OutboundMediaChan() <-chan OutboundMediaMessage {
func (mb *MessageBus) Close() { func (mb *MessageBus) Close() {
mb.closeOnce.Do(func() { mb.closeOnce.Do(func() {
mb.closed.Store(true)
// notify all blocked publishers to exit // notify all blocked publishers to exit
close(mb.done) close(mb.done)
// because every publisher will check mb.closed before acquiring wg
// so we can be sure that new publishers will not be added new messages after this point
mb.closed.Store(true)
// wait for all ongoing Publish calls to finish, ensuring all messages have been sent to channels or exited // wait for all ongoing Publish calls to finish, ensuring all messages have been sent to channels or exited
mb.wg.Wait() mb.wg.Wait()

View file

@ -43,14 +43,19 @@ func TestHandleIncoming_DoesNotConsumeGenericCommandsLocally(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel() defer cancel()
inbound, ok := messageBus.ConsumeInbound(ctx) select {
if !ok { case <-ctx.Done():
t.Fatal("expected inbound message to be forwarded") t.Fatal("timeout waiting for message to be forwarded")
} return
if inbound.Channel != "whatsapp_native" { case inbound, ok := <-messageBus.InboundChan():
t.Fatalf("channel=%q", inbound.Channel) if !ok {
} t.Fatal("expected inbound message to be forwarded")
if inbound.Content != "/new" { }
t.Fatalf("content=%q", inbound.Content) if inbound.Channel != "whatsapp_native" {
t.Fatalf("channel=%q", inbound.Channel)
}
if inbound.Content != "/new" {
t.Fatalf("content=%q", inbound.Content)
}
} }
} }