Merge pull request #13 from dj-oyu/fix/heartbeat-completion-message

fix: heartbeat completion shows LLM result instead of garbled streaming fragment
This commit is contained in:
dj-oyu 2026-03-02 04:03:08 +09:00 committed by GitHub
commit 18d1b6e807

View file

@ -43,6 +43,7 @@ import (
// activeTask tracks a running agent task for live status and intervention. // activeTask tracks a running agent task for live status and intervention.
type activeTask struct { type activeTask struct {
Description string Description string
Result string // LLM response summary for completion notification
Iteration int Iteration int
MaxIter int MaxIter int
StartedAt time.Time StartedAt time.Time
@ -52,6 +53,7 @@ type activeTask struct {
lastError *toolLogEntry // sticky: most recent error, persists across iterations lastError *toolLogEntry // sticky: most recent error, persists across iterations
projectDir string // detected from exec cd target (authoritative) projectDir string // detected from exec cd target (authoritative)
fileCommonDir string // LCP of file paths relative to workspace (fallback) fileCommonDir string // LCP of file paths relative to workspace (fallback)
streamedChunks bool // true after onChunk fires at least once
mu sync.Mutex mu sync.Mutex
} }
@ -1054,11 +1056,15 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
// Publish final task status on completion for background tasks // Publish final task status on completion for background tasks
if opts.TaskID != "" { if opts.TaskID != "" {
elapsed := time.Since(task.StartedAt) elapsed := time.Since(task.StartedAt)
summary := task.Result
if summary == "" {
summary = task.Description
}
doneCtx, doneCancel := context.WithTimeout(context.Background(), 5*time.Second) doneCtx, doneCancel := context.WithTimeout(context.Background(), 5*time.Second)
_ = al.bus.PublishOutbound(doneCtx, bus.OutboundMessage{ _ = al.bus.PublishOutbound(doneCtx, bus.OutboundMessage{
Channel: opts.Channel, Channel: opts.Channel,
ChatID: opts.ChatID, ChatID: opts.ChatID,
Content: fmt.Sprintf("\u2705 Task completed (%.1fs)\n%s", elapsed.Seconds(), task.Description), Content: fmt.Sprintf("\u2705 Task completed (%.1fs)\n%s", elapsed.Seconds(), summary),
IsTaskStatus: true, IsTaskStatus: true,
TaskID: opts.TaskID, TaskID: opts.TaskID,
}) })
@ -1343,6 +1349,24 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
finalContent = opts.DefaultResponse finalContent = opts.DefaultResponse
} }
// 5d. Store result summary for task completion notification
if task != nil {
task.Result = utils.Truncate(finalContent, 280)
}
// 5e. Replace orphaned streaming status bubble 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.
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,
})
}
// 6. Save final assistant message to session (deferred write-behind) // 6. Save final assistant message to session (deferred write-behind)
agent.Sessions.AddMessage(opts.SessionKey, "assistant", finalContent) agent.Sessions.AddMessage(opts.SessionKey, "assistant", finalContent)
agent.Sessions.MarkDirty(opts.SessionKey) agent.Sessions.MarkDirty(opts.SessionKey)
@ -2175,6 +2199,9 @@ func (al *AgentLoop) runLLMIteration(
if !constants.IsInternalChannel(opts.Channel) { if !constants.IsInternalChannel(opts.Channel) {
lastPublish := time.Time{} lastPublish := time.Time{}
onChunk = func(accumulated, reasoning string) { onChunk = func(accumulated, reasoning string) {
if task != nil {
task.streamedChunks = true
}
if time.Since(lastPublish) < 500*time.Millisecond { if time.Since(lastPublish) < 500*time.Millisecond {
return return
} }