fix: redirect message tool to task status bubble for background tasks

Root cause: LLM calls the `message` tool during heartbeat execution,
which publishes directly to the bus via sendCallback, bypassing the
SendResponse=false guard. This creates a second bubble separate from
the task status.

Fix: For background tasks with SendResponse=false (e.g. heartbeat),
replace the message tool's sendCallback to publish as IsTaskStatus
with the same TaskID. This routes message tool output into the same
bubble as streaming preview and task completion.

Also removes the now-unnecessary PromoteStatusToTask call since
streaming preview already uses IsTaskStatus directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-03-03 11:40:06 +09:00
parent 1804c85102
commit 0cea7ed32f

View file

@ -1127,6 +1127,28 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
// 1. Update tool contexts
al.updateToolContexts(agent, opts.Channel, opts.ChatID)
// 1-bis. For background tasks that don't send a final response (e.g. heartbeat),
// redirect the message tool to publish as IsTaskStatus so its output lands in
// the same bubble as the task status instead of creating a separate message.
if opts.Background && !opts.SendResponse && opts.TaskID != "" {
if tool, ok := agent.Tools.Get("message"); ok {
if mt, ok := tool.(*tools.MessageTool); ok {
taskID := opts.TaskID
mt.SetSendCallback(func(channel, chatID, content string) error {
pubCtx, pubCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer pubCancel()
return al.bus.PublishOutbound(pubCtx, bus.OutboundMessage{
Channel: channel,
ChatID: chatID,
Content: content,
IsTaskStatus: true,
TaskID: taskID,
})
})
}
}
}
// 1a. Set session-specific working directory for bootstrap file lookup.
// Prefer the tool-detected project directory (touch_dir) from the session tracker,
// resolved as an absolute path under workspace. Fall back to worktree or workspace.
@ -1391,17 +1413,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
task.Result = utils.Truncate(finalContent, 280)
}
// 5e. Promote streaming status bubble to task message for background tasks.
// When SendResponse is false (e.g. heartbeat), no final non-status message
// 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 && opts.TaskID != "" {
statusKey := opts.Channel + ":" + opts.ChatID
al.channelManager.PromoteStatusToTask(statusKey, opts.TaskID)
}
// 6. Save final assistant message to session (deferred write-behind)
agent.Sessions.AddMessage(opts.SessionKey, "assistant", finalContent)