fix: prevent session poisoning from null/error LLM responses

When an LLM returns content: null on a plain assistant message (no
tool_calls), the null gets saved to session history and causes
permanent 400 errors on every subsequent request.

- Sanitize None content on plain assistant messages to "(empty)" in
  _sanitize_empty_content(), matching the existing empty-string handling
- Skip persisting error responses (finish_reason="error") to the
  message history in _run_agent_loop(), preventing poison loops

Closes #1303
This commit is contained in:
Nikolas de Hor
2026-02-28 00:57:08 -03:00
parent a4d95fd064
commit 66063abb8c
2 changed files with 14 additions and 0 deletions

View File

@@ -224,6 +224,12 @@ class AgentLoop:
) )
else: else:
clean = self._strip_think(response.content) clean = self._strip_think(response.content)
# Don't persist error responses to session history — they can
# poison the context and cause permanent 400 loops (#1303).
if response.finish_reason == "error":
logger.error("LLM returned error: {}", (clean or "")[:200])
final_content = clean or "Sorry, I encountered an error calling the AI model."
break
messages = self.context.add_assistant_message( messages = self.context.add_assistant_message(
messages, clean, reasoning_content=response.reasoning_content, messages, clean, reasoning_content=response.reasoning_content,
) )

View File

@@ -51,6 +51,14 @@ class LLMProvider(ABC):
for msg in messages: for msg in messages:
content = msg.get("content") content = msg.get("content")
# None content on a plain assistant message (no tool_calls) crashes
# providers with "invalid message content type: <nil>".
if content is None and msg.get("role") == "assistant" and not msg.get("tool_calls"):
clean = dict(msg)
clean["content"] = "(empty)"
result.append(clean)
continue
if isinstance(content, str) and not content: if isinstance(content, str) and not content:
clean = dict(msg) clean = dict(msg)
clean["content"] = None if (msg.get("role") == "assistant" and msg.get("tool_calls")) else "(empty)" clean["content"] = None if (msg.get("role") == "assistant" and msg.get("tool_calls")) else "(empty)"