7965462d6c
The compaction summary's role was selected against the LITERAL neighbouring messages (compressed[-1] / tail_messages[0]). Mistral-family chat templates (Devstral, Mistral Small 3.x, Magistral) enforce user/assistant alternation but exempt the tool flow (tool results and assistant messages carrying tool_calls) from the check, so a protected head ending [user, assistant(tool_calls), tool] pinned the summary to role="user" while the last role the template counts is "user": the backend rejects the whole request with a Jinja alternation error (HTTP 500). The summary persists in the stored conversation, every retry replays the identical poisoned history, and the session is permanently unrecoverable. Fires on EVERY compaction against a Mistral-strict backend, captured byte-exact via a tee-proxy in front of a llama.cpp/llama-swap Devstral deployment. Fix: compute both neighbour roles through _template_visible_role(), which skips template-exempt messages. The #52160 (Anthropic user-first) and #58753 (zero-user-turn) forced-user guards are preserved; their forced shapes (summary-user followed only by exempt messages) are alternation-safe. When the visible head ends "assistant" and the visible tail opens "user", no standalone role can alternate and the existing merge-into-tail fallback now correctly fires (the literal logic emitted a standalone user summary there: a second poisoning shape). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGN45sMMbwM8cW9T9ga4ou