Fix: OpenHands BadRequestError: Mistral Alternating Roles
OpenHands crashes with a BadRequestError after about 10 minutes of runtime.
Quick Take
- OpenHands fails with “BadRequestError: Alternating roles required” when Mistral Small 4 sees two USER messages in a row
- The bug lives in agent_controller.py where RecallAction gets dispatched with the same role as the triggering MessageAction
- Three independent fixes are needed: patch the controller, disable prompt extensions, and mount a custom system prompt
The BadRequestError Explained
The error message is brutal and precise:
BadRequestError: LLM responded with error:
Alternating roles required: messages must alternate user/assistant
Mistral Small 4 enforces strict role alternation. When OpenHands sends two USER messages consecutively, the LLM refuses to respond.
Here’s what happens inside OpenHands:
Event 139: MessageAction (source=USER) ← User sends a message
Event 140: RecallAction (source=USER) ← OpenHands tries to recall context
Two USER events in a row. The LLM throws up its hands.
Why Two USER Events Happen
The root cause is in _handle_message_action. After a user message, OpenHands dispatches a RecallAction with EventSource.USER and recall_type: KNOWLEDGE. This recall is meant to fetch context, but it violates Mistral’s role alternation rule.
The code path looks like this:
# agent_controller.py, _handle_message_action
if action.recall_type == RecallType.KNOWLEDGE:
await self.dispatch_event(
RecallAction(
source=EventSource.USER, # ← This is the problem
recall_type=RecallType.KNOWLEDGE,
)
)
Mistral Small 4 expects: USER → ASSISTANT → USER → ASSISTANT. Two USERs in a row breaks the contract.
Patch the Controller Directly
The fastest fix is to patch agent_controller.py to skip the recall when prompt extensions are disabled.
# Patch in /data/openhands-state/patches/agent_controller.py
# Inside _handle_message_action, before the RecallAction block
if not self.agent.config.enable_prompt_extensions:
if self.get_agent_state() != AgentState.RUNNING:
await self.set_agent_state_to(AgentState.RUNNING)
return
Mount the patched file into the container:
docker run -v /data/openhands-state/patches/agent_controller.py:/app/openhands/controller/agent_controller.py:ro ...
This patch prevents the second USER event from being dispatched when prompt extensions are off.
Note: This patch survives container restarts but not image updates. You’ll need to reapply it after each upgrade.
Disable Prompt Extensions in config.toml
OpenHands 0.59.0 changed how prompt extensions work. The old field system_prompt_addition is gone. Using it now silently drops the entire [agent] section and reverts to defaults.
# config.toml
[agent]
enable_prompt_extensions = false
system_prompt_filename = "custom_system_prompt.md"
Watch out: If you copy-paste an old config with system_prompt_addition, OpenHands will ignore the [agent] section completely. The logs show:
docker logs openhands 2>&1 | grep "Using defaults"
# If you see this, your config is broken
Validate the config with:
docker exec openhands python3 -c "
from openhands.core.config.agent_config import AgentConfig
print(list(AgentConfig.model_fields.keys()))"
If system_prompt_addition appears in the output, your config is invalid and the bug will return.
Mount a Custom System Prompt
OpenHands looks for the system prompt at /app/openhands/agenthub/codeact_agent/prompts/custom_system_prompt.md. Mount your custom prompt there:
docker run \
-v /data/openhands-state/system_prompt.md:/app/openhands/agenthub/codeact_agent/prompts/custom_system_prompt.md:ro \
...
The prompt should include clear instructions for Mistral Small 4, like:
You are a senior engineer debugging OpenHands. Follow these steps:
1. Triage the issue
2. Write a failing test
3. Fix the code
4. Verify the fix
This keeps the conversation focused and avoids extra recall actions that could trigger the role alternation error.
Critical Gotchas and Limitations
Note: The patch in agent_controller.py only works when enable_prompt_extensions is false. If you re-enable extensions later, the bug returns.
Warning: OpenHands 0.59.0+ silently ignores invalid config fields in [agent]. Double-check your config with the Python snippet above before deploying.
Gotcha: The system prompt filename must match exactly what OpenHands expects inside the container. A typo in the path or filename breaks the prompt loading silently.
Watch out: After an image update, the patched agent_controller.py will be replaced. You must reapply the patch and restart the container. The recreate script helps:
sudo bash /data/scripts/recreate-openhands.sh
What I Actually Use
- Mistral Small 4: The model that enforces strict role alternation and exposed this bug
- DGX Spark ARM64 server: Handles the Mistral Small 4 workload without throttling
- SearXNG: Provides runtime context without leaking queries to big tech
OpenHands Role Alternation Fix
Problem to resolution workflow