p0: purge orphan tool outputs on pairing error to prevent repeated 400 loops

This commit is contained in:
DBT
2026-02-26 13:45:41 +00:00
parent 0515a1247f
commit 36ea7486d1
2 changed files with 85 additions and 0 deletions

View File

@@ -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,

View File

@@ -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]