A chat session works fine, then one turn fails with
400 role 'system' must follow a 'user' message
and from that point on every turn fails identically. Short typed messages fail. Nothing recovers it except resetting the conversation. The trigger in the report was a run of long pastes that aged past the window the history pruner keeps (openclaw/openclaw#172241).
Why it happens
Providers with strict message-ordering rules do not just validate the message you send. They validate the history you rebuilt. Anything that trims a transcript - compaction, media pruning, a keep-recent-turns safeguard - can drop a turn that an injected system marker was anchored to and leave the marker sitting after an assistant message instead of in a position the provider accepts.
The failure is sticky for a specific reason. The rewrite is deterministic. The same stored transcript produces the same broken history on the next turn, and the turn after that. It looks like the account, the model or the request is broken, when the payload was poisoned one turn earlier and is being rebuilt faithfully every time.
What to do
When a rewrite step removes turns, treat injected marker messages as belonging to a user turn. Merge a marker into the next surviving user turn, or drop it when the turn it was attached to is gone. Never leave one stranded between two assistant turns or at the end of the history.
// after pruning, before sending
for (const msg of history) {
if (msg.role === 'system' && !followsUser(msg, history)) {
mergeIntoNextUser(msg, history) || drop(msg, history)
}
}
If you are staring at this 400 right now, do not retry. The request will fail the same way until the conversation is reset, because the poisoned history is still there. Reset it, then add a check on the rewrite boundary so the ordering rule is asserted before the payload goes out rather than being discovered as a 400 mid-session.