From 36ea7486d1a3efee71890b6ff48178f48b9c45a1 Mon Sep 17 00:00:00 2001 From: DBT Date: Thu, 26 Feb 2026 13:45:41 +0000 Subject: [PATCH] p0: purge orphan tool outputs on pairing error to prevent repeated 400 loops --- pkg/agent/loop.go | 10 ++++++ pkg/session/manager.go | 75 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 1f355b6..612b828 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -611,6 +611,11 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) } if err != nil { + errText := strings.ToLower(err.Error()) + if strings.Contains(errText, "no tool call found for function call output") { + removed := al.sessions.PurgeOrphanToolOutputs(msg.SessionKey) + logger.WarnCF("agent", "Purged orphan tool outputs after provider pairing error", map[string]interface{}{"session_key": msg.SessionKey, "removed": removed}) + } logger.ErrorCF("agent", "LLM call failed", map[string]interface{}{ "iteration": iteration, @@ -885,6 +890,11 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe }) if err != nil { + errText := strings.ToLower(err.Error()) + if strings.Contains(errText, "no tool call found for function call output") { + removed := al.sessions.PurgeOrphanToolOutputs(sessionKey) + logger.WarnCF("agent", "Purged orphan tool outputs after provider pairing error (system)", map[string]interface{}{"session_key": sessionKey, "removed": removed}) + } logger.ErrorCF("agent", "LLM call failed in system message", map[string]interface{}{ "iteration": iteration, diff --git a/pkg/session/manager.go b/pkg/session/manager.go index 2c4672a..cab1e6b 100644 --- a/pkg/session/manager.go +++ b/pkg/session/manager.go @@ -246,6 +246,81 @@ func (sm *SessionManager) SetPreferredLanguage(key, lang string) { session.mu.Unlock() } +func (sm *SessionManager) PurgeOrphanToolOutputs(key string) int { + sm.mu.RLock() + session, ok := sm.sessions[key] + sm.mu.RUnlock() + if !ok { + return 0 + } + + session.mu.Lock() + defer session.mu.Unlock() + pending := map[string]struct{}{} + kept := make([]providers.Message, 0, len(session.Messages)) + removed := 0 + for _, m := range session.Messages { + role := strings.ToLower(strings.TrimSpace(m.Role)) + switch role { + case "assistant": + for _, tc := range m.ToolCalls { + id := strings.TrimSpace(tc.ID) + if id != "" { + pending[id] = struct{}{} + } + } + kept = append(kept, m) + case "tool": + id := strings.TrimSpace(m.ToolCallID) + if id == "" { + removed++ + continue + } + if _, ok := pending[id]; !ok { + removed++ + continue + } + delete(pending, id) + kept = append(kept, m) + default: + kept = append(kept, m) + } + } + if removed == 0 { + return 0 + } + session.Messages = kept + session.Updated = time.Now() + if sm.storage != "" { + _ = sm.rewriteSessionFileLocked(session) + _ = sm.writeOpenClawSessionsIndex() + } + return removed +} + +func (sm *SessionManager) rewriteSessionFileLocked(session *Session) error { + if sm.storage == "" || session == nil { + return nil + } + path := filepath.Join(sm.storage, session.Key+".jsonl") + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + for _, msg := range session.Messages { + e := toOpenClawMessageEvent(msg) + b, err := json.Marshal(e) + if err != nil { + continue + } + if _, err := f.Write(append(b, '\n')); err != nil { + return err + } + } + return nil +} + func (sm *SessionManager) TruncateHistory(key string, keepLast int) { sm.mu.RLock() session, ok := sm.sessions[key]