OpenHands crashes after 10 minutes with a BadRequestError. Here’s exactly how to fix the alternating roles bug in Mistral Small 4 and why the default config is broken.

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
Flow

OpenHands Role Alternation Fix

Problem to resolution workflow

1
Error Occurs BadRequestError: Alternating roles required
2
Root Cause Two USER messages in sequence
3
Problem Location agent_controller.py RecallAction
4
First Fix Patch controller to skip recall
5
Second Fix Disable prompt extensions
6
Third Fix Mount custom system prompt
Illustration: Fix: OpenHands BadRequestError: Mistral Alternating Roles