fix: heartbeat completion shows LLM result instead of garbled streaming fragment
Two issues with heartbeat (SendResponse: false) tasks: 1. The last streaming status bubble persists as a garbled fragment because no final non-status message triggers preSend cleanup. 2. The "Task completed" notification shows the heartbeat prompt instead of the LLM response. Add Result/streamedChunks fields to activeTask, store the LLM response summary after the loop, publish a clean status replacement for background tasks that streamed, and use task.Result in the completion notification. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8336d3f6dc
commit
dd869d20dd
1 changed files with 38 additions and 12 deletions
|
|
@ -42,17 +42,19 @@ 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
|
||||||
Iteration int
|
Result string // LLM response summary for completion notification
|
||||||
MaxIter int
|
Iteration int
|
||||||
StartedAt time.Time
|
MaxIter int
|
||||||
cancel context.CancelFunc
|
StartedAt time.Time
|
||||||
interrupt chan string // buffered 1, for user message injection
|
cancel context.CancelFunc
|
||||||
toolLog []toolLogEntry
|
interrupt chan string // buffered 1, for user message injection
|
||||||
lastError *toolLogEntry // sticky: most recent error, persists across iterations
|
toolLog []toolLogEntry
|
||||||
projectDir string // detected from exec cd target (authoritative)
|
lastError *toolLogEntry // sticky: most recent error, persists across iterations
|
||||||
fileCommonDir string // LCP of file paths relative to workspace (fallback)
|
projectDir string // detected from exec cd target (authoritative)
|
||||||
mu sync.Mutex
|
fileCommonDir string // LCP of file paths relative to workspace (fallback)
|
||||||
|
streamedChunks bool // true after onChunk fires at least once
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// toolLogEntry records a single tool call for the live terminal view.
|
// toolLogEntry records a single tool call for the live terminal view.
|
||||||
|
|
@ -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,23 @@ 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 +2198,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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue