fix(delegation): add pending tasks hint when team_task_id not found

LLM models often hallucinate UUIDs when delegating, passing a wrong
team_task_id that doesn't exist. Previously the error was bare
("task not found") with no guidance, causing the model to get stuck.

Now the error includes a list of pending tasks so the model can
self-correct. Also refactored prepareDelegation to resolve team once
instead of 3 separate GetTeamForAgent calls, and extracted
pendingTasksHint() to deduplicate hint-building logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
viettranxandClaude Opus 4.6 committed 2026-03-03 17:41:09 +07:00
1 parent 37158af231
commit dbd317ac81
4 files changed
+126 -31

No files matched your search

+34
View File
@@ -107,6 +107,9 @@ type Loop struct {
// Group writer cache for system prompt injection (managed mode)
groupWriterCache *store.GroupWriterCache
// Team store for cross-session pending task detection (managed mode)
teamStore store.TeamStore
}
// AgentEvent is emitted during agent execution for WS broadcasting.
@@ -175,6 +178,9 @@ type LoopConfig struct {
// Group writer cache for system prompt injection (managed mode)
GroupWriterCache *store.GroupWriterCache
// Team store for cross-session pending task detection (managed mode)
TeamStore store.TeamStore
}
func NewLoop(cfg LoopConfig) *Loop {
@@ -235,6 +241,7 @@ func NewLoop(cfg LoopConfig) *Loop {
builtinToolSettings: cfg.BuiltinToolSettings,
thinkingLevel: cfg.ThinkingLevel,
groupWriterCache: cfg.GroupWriterCache,
teamStore: cfg.TeamStore,
}
}
@@ -530,6 +537,33 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
}
}
// 2b. Cross-session recovery: notify team leads about orphaned pending tasks.
// Safe because Bước 1 (early ClaimTask) ensures running tasks are in_progress,
// so only truly un-spawned tasks remain pending.
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", "", ""); err == nil {
var stale []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 len(stale) > 0 {
reminder := fmt.Sprintf(
"[System] You have %d pending team task(s) that were never spawned:\n%s\n"+
"Spawn each one, or cancel with team_tasks action=cancel if no longer needed.",
len(stale), strings.Join(stale, "\n"))
messages = append(messages,
providers.Message{Role: "user", Content: reminder},
providers.Message{Role: "assistant", Content: "I see the pending tasks. Let me handle them."},
)
}
}
}
}
// 3. Buffer new messages — write to session only AFTER the run completes.
// This prevents concurrent runs from seeing each other's in-progress messages.
// NOTE: pendingMsgs stores TEXT ONLY (no images) to avoid bloating session storage.