@@ -792,30 +792,7 @@ If not, please feel free to ignore. Again do not mention this message to the use
792792 continue
793793 }
794794 if m.Role == message.Tool {
795− // Filter out tool results that have no matching tool call. An orphaned
796− // result causes every subsequent API call to fail validation.
797− aiMsgs := m.ToAIMessage()
798− if len(aiMsgs) == 0 {
799− continue
800− }
801− var validParts []fantasy.MessagePart
802− for _, part := range aiMsgs[0].Content {
803− tr, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
804− if !ok {
805− validParts = append(validParts, part)
806− continue
807− }
808− if _, known := knownToolCallIDs[tr.ToolCallID]; known {
809− validParts = append(validParts, part)
810− } else {
811− slog.Warn("Dropping orphaned tool result with no matching tool call",
812− "tool_call_id", tr.ToolCallID,
813− )
814− }
815− }
816− if len(validParts) > 0 {
817− msg := aiMsgs[0]
818− msg.Content = validParts
795+ if msg, ok := filterOrphanedToolResults(m, knownToolCallIDs); ok {
819796 history = append(history, msg)
820797 }
821798 continue
@@ -838,6 +815,39 @@ If not, please feel free to ignore. Again do not mention this message to the use
838815 return history, files
839816}
840817
818+// filterOrphanedToolResults converts a tool message to a fantasy.Message,
819+// dropping any tool result parts whose tool_call_id has no matching tool call
820+// in the known set. An orphaned result causes API validation to fail on every
821+// subsequent turn, permanently locking the session. Returns the filtered
822+// message and true if at least one valid part remains.
823+func filterOrphanedToolResults(m message.Message, knownToolCallIDs map[string]struct{}) (fantasy.Message, bool) {
824+ aiMsgs := m.ToAIMessage()
825+ if len(aiMsgs) == 0 {
826+ return fantasy.Message{}, false
827+ }
828+ var validParts []fantasy.MessagePart
829+ for _, part := range aiMsgs[0].Content {
830+ tr, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
831+ if !ok {
832+ validParts = append(validParts, part)
833+ continue
834+ }
835+ if _, known := knownToolCallIDs[tr.ToolCallID]; known {
836+ validParts = append(validParts, part)
837+ } else {
838+ slog.Warn("Dropping orphaned tool result with no matching tool call",
839+ "tool_call_id", tr.ToolCallID,
840+ )
841+ }
842+ }
843+ if len(validParts) == 0 {
844+ return fantasy.Message{}, false
845+ }
846+ msg := aiMsgs[0]
847+ msg.Content = validParts
848+ return msg, true
849+}
850+
841851func (a *sessionAgent) getSessionMessages(ctx context.Context, session session.Session) ([]message.Message, error) {
842852 msgs, err := a.messages.List(ctx, session.ID)
843853 if err != nil {