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

My OpenHands container crashed with a BadRequestError after about 10 minutes of runtime, every time, on the same workload that ran fine the day before. The root cause was not in the workload, the OpenHands version, or the Mistral model I was pointing it at. It was in how OpenHands was building the request body that SGLang then rejected. Here is the trace I followed and the one config knob that closed the crash.

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

Why this fix is also the OpenClaw fix

The same BadRequestError from alternating-roles is the canonical failure mode for any agent framework pointed at a Mistral inference server through SGLang. OpenHands surfaces it via the enable_prompt_extensions = false config knob; OpenClaw needs a different mechanism (the Side-Car-Proxy that rewrites incoming requests before SGLang sees them). The root cause is identical: framework-injected system messages produce alternating-role conversations that Mistral rejects.

If you are setting up a new agent framework against the same SGLang endpoint, the diagnostic is always the same: turn on debug logging on the framework side, look for back-to-back assistant or back-to-back user role entries in the request body, and either disable the framework feature that injected them or proxy-rewrite the body. Whichever path the framework supports.

The deeper lesson is that “BadRequest” alone is not actionable. The real signal is in the request body before SGLang sees it; configure framework logging to capture that body once, fix the role-ordering, then move on. Re-debugging this from scratch per framework wastes hours.

Upstream status

Filed as an issue at the OpenHands tracker on 2026-05-04: issue #14287. The body proposes three fixes (drop EventSource.USER on the RecallAction, merge consecutive same-role messages at request-build time, or a per-provider requires_strict_alternation capability flag) and offers a PR for whichever direction maintainers prefer. Future progress lands here as a status update. The full list of contributions made while running this stack: /upstream/.

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

Was this worth it? Zap the article.

Value for value, no signup. Sats go straight to the writer.