Merge pull request #16 from dj-oyu/fix/heartbeat-duplicate-completion

fix: prevent duplicate heartbeat completion messages
This commit is contained in:
dj-oyu 2026-03-02 04:41:34 +09:00 committed by GitHub
commit 504258f90c
2 changed files with 21 additions and 9 deletions

View file

@ -1354,17 +1354,16 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
task.Result = utils.Truncate(finalContent, 280)
}
// 5e. Replace orphaned streaming status bubble for background tasks.
// 5e. Promote streaming status bubble to task message for background tasks.
// When SendResponse is false (e.g. heartbeat), no final non-status message
// triggers cleanup, so the last streaming chunk persists on Telegram.
// triggers preSend's statusMsgIDs.LoadAndDelete cleanup, so the last
// streaming chunk persists on Telegram. Move the tracked status message ID
// into taskMsgIDs so the defer's IsTaskStatus completion notification edits
// the existing bubble instead of creating a duplicate message.
if opts.Background && !opts.SendResponse && !constants.IsInternalChannel(opts.Channel) && task != nil &&
task.streamedChunks {
_ = al.bus.PublishOutbound(ctx, bus.OutboundMessage{
Channel: opts.Channel,
ChatID: opts.ChatID,
Content: utils.Truncate(finalContent, 200),
IsStatus: true,
})
task.streamedChunks && opts.TaskID != "" {
statusKey := opts.Channel + ":" + opts.ChatID
al.channelManager.PromoteStatusToTask(statusKey, opts.TaskID)
}
// 6. Save final assistant message to session (deferred write-behind)

View file

@ -839,6 +839,19 @@ func (m *Manager) runTTLJanitor(ctx context.Context) {
}
}
// PromoteStatusToTask moves the tracked streaming status message for the given
// channel:chatID key into the task message map under taskID. This allows the
// next IsTaskStatus publish to edit the streaming bubble instead of creating a
// new message. Returns true if a status message was found and promoted.
func (m *Manager) PromoteStatusToTask(statusKey, taskID string) bool {
v, loaded := m.statusMsgIDs.LoadAndDelete(statusKey)
if !loaded {
return false
}
m.taskMsgIDs.Store(taskID, v)
return true
}
func (m *Manager) GetChannel(name string) (Channel, bool) {
m.mu.RLock()
defer m.mu.RUnlock()