Files
goclaw/internal/agent/loop_team_reminders.go
viettranx 014f74ec15 fix(agent): group session unresponsive during team task execution (#266)
Two fixes:

1. Remove assistant prefill from team task reminders. The injected
   [user]+[assistant]+[user] pattern caused LLMs to treat the canned
   ack as "turn complete", returning NO_REPLY for every user message
   in group sessions with active tasks. Reminders are now merged into
   the user message as prefix tags.

2. Add PeerKind propagation to team notification routing. TaskTicker
   and progress notifications were missing PeerKind on InboundMessage,
   causing them to route to phantom DM sessions instead of the correct
   group session. PeerKind is now carried through event payloads,
   notify queue metadata, and all inbound message publications.
2026-03-30 15:20:01 +07:00

105 lines
4.4 KiB
Go

package agent
import (
"context"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/providers"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// memberTaskInfo holds cached task metadata for mid-loop progress nudges.
type memberTaskInfo struct {
Subject string
TaskNumber int
}
// injectTeamTaskReminders adds leader pending-task reminders and
// member progress context to the message list before the first LLM call.
// Returns updated messages and member task info (for progress nudges).
func (l *Loop) injectTeamTaskReminders(ctx context.Context, req *RunRequest, messages []providers.Message) ([]providers.Message, memberTaskInfo) {
// 2g. Cross-session task reminder: notify team leads about pending and in-progress tasks.
// Stale recovery (expired lock → pending) is handled by the background TaskTicker.
// Reminders are injected BEFORE the user message so the user's actual message is always
// the last message — prevents trailing assistant messages that proxy providers reject.
if l.teamStore != nil && l.agentUUID != uuid.Nil {
if team, _ := l.teamStore.GetTeamForAgent(ctx, l.agentUUID); team != nil && team.LeadAgentID == l.agentUUID {
if tasks, err := l.teamStore.ListTasks(ctx, team.ID, "newest", "active", req.UserID, "", "", 0, 0); err == nil {
var stale []string
var inProgress []string
for _, t := range tasks {
if t.Status == store.TeamTaskStatusPending {
age := time.Since(t.CreatedAt).Truncate(time.Minute)
stale = append(stale, fmt.Sprintf("- %s: \"%s\" (pending %s)", t.ID, t.Subject, age))
}
if t.Status == store.TeamTaskStatusInProgress {
age := time.Since(t.UpdatedAt).Truncate(time.Minute)
progressInfo := fmt.Sprintf("in progress %s", age)
if t.ProgressPercent > 0 {
if t.ProgressStep != "" {
progressInfo = fmt.Sprintf("%d%% — %s, %s", t.ProgressPercent, t.ProgressStep, age)
} else {
progressInfo = fmt.Sprintf("%d%%, %s", t.ProgressPercent, age)
}
}
inProgress = append(inProgress, fmt.Sprintf("- %s: \"%s\" (%s)", t.ID, t.Subject, progressInfo))
}
}
var parts []string
if len(stale) > 0 {
parts = append(parts, fmt.Sprintf(
"You have %d pending team task(s) awaiting dispatch:\n%s\n"+
"These tasks will be auto-dispatched to available team members. If no longer needed, cancel with team_tasks action=cancel.",
len(stale), strings.Join(stale, "\n")))
}
if len(inProgress) > 0 {
parts = append(parts, fmt.Sprintf(
"You have %d in-progress team task(s) being handled by team members:\n%s\n"+
"Their results will arrive automatically. Do NOT cancel, re-create, or re-spawn these tasks.",
len(inProgress), strings.Join(inProgress, "\n")))
}
if len(parts) > 0 {
reminder := "[System] " + strings.Join(parts, "\n\n")
// Merge reminder into the user message as a prefix tag.
// Previous approach injected [user]+[assistant]+[user] which caused
// LLMs to treat the assistant ack as "turn complete" → NO_REPLY (#266).
userMsg := messages[len(messages)-1]
messages[len(messages)-1] = providers.Message{
Role: "user",
Content: "[Active team tasks]\n" + reminder + "\n[/Active team tasks]\n\n" + userMsg.Content,
}
}
}
}
}
// 2h. Member task reminder: inject task context for members working on dispatched tasks.
// Caches task subject/number for mid-loop progress nudge (avoids extra DB query).
var info memberTaskInfo
if req.TeamTaskID != "" && l.teamStore != nil {
if taskUUID, err := uuid.Parse(req.TeamTaskID); err == nil {
if task, err := l.teamStore.GetTask(ctx, taskUUID); err == nil && task != nil {
info.Subject = task.Subject
info.TaskNumber = task.TaskNumber
reminder := fmt.Sprintf(
"[System] You are working on team task #%d: %q. "+
"Stay focused on this task. Your final response becomes the task result — make it clear and complete. "+
"For long tasks, report progress: team_tasks(action=\"progress\", percent=50, text=\"status\").",
task.TaskNumber, task.Subject)
// Merge reminder into user message as prefix tag (#266).
userMsg := messages[len(messages)-1]
messages[len(messages)-1] = providers.Message{
Role: "user",
Content: "[Task context]\n" + reminder + "\n[/Task context]\n\n" + userMsg.Content,
}
}
}
}
return messages, info
}