@@ -792,30 +792,7 @@ If not, please feel free to ignore. Again do not mention this message to the use
794 if m.Role == message.Tool {
794 if m.Role == message.Tool {
795− // Filter out tool results that have no matching tool call. An orphaned
795+ if msg, ok := filterOrphanedToolResults(m, knownToolCallIDs); ok {
796− // result causes every subsequent API call to fail validation.
797− aiMsgs := m.ToAIMessage()
798− if len(aiMsgs) == 0 {
801− var validParts []fantasy.MessagePart
802− for _, part := range aiMsgs[0].Content {
803− tr, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
805− validParts = append(validParts, part)
808− if _, known := knownToolCallIDs[tr.ToolCallID]; known {
809− validParts = append(validParts, part)
811− slog.Warn("Dropping orphaned tool result with no matching tool call",
812− "tool_call_id", tr.ToolCallID,
816− if len(validParts) > 0 {
818− msg.Content = validParts
819 history = append(history, msg)
796 history = append(history, msg)
@@ -838,6 +815,39 @@ If not, please feel free to ignore. Again do not mention this message to the use
838 return history, files
815 return history, files
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
828+ var validParts []fantasy.MessagePart
829+ for _, part := range aiMsgs[0].Content {
830+ tr, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
832+ validParts = append(validParts, part)
835+ if _, known := knownToolCallIDs[tr.ToolCallID]; known {
836+ validParts = append(validParts, part)
838+ slog.Warn("Dropping orphaned tool result with no matching tool call",
839+ "tool_call_id", tr.ToolCallID,
843+ if len(validParts) == 0 {
844+ return fantasy.Message{}, false
847+ msg.Content = validParts
841func (a *sessionAgent) getSessionMessages(ctx context.Context, session session.Session) ([]message.Message, error) {
851func (a *sessionAgent) getSessionMessages(ctx context.Context, session session.Session) ([]message.Message, error) {
842 msgs, err := a.messages.List(ctx, session.ID)
852 msgs, err := a.messages.List(ctx, session.ID)
843 if err != nil {
853 if err != nil {