{"record":{"id":"7e3e80e4593ef69c","repo":"Aider-AI/aider","slug":"messages-don-t-properly-alternate-user-assistant","errorCode":null,"errorMessage":"Messages don't properly alternate user/assistant:\n\n{turns}","messagePattern":"Messages don't properly alternate user/assistant:\n\n(.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"aider/sendchat.py","lineNumber":20,"sourceCode":"from aider.utils import format_messages\n\n\ndef sanity_check_messages(messages):\n    \"\"\"Check if messages alternate between user and assistant roles.\n    System messages can be interspersed anywhere.\n    Also verifies the last non-system message is from the user.\n    Returns True if valid, False otherwise.\"\"\"\n    last_role = None\n    last_non_system_role = None\n\n    for msg in messages:\n        role = msg.get(\"role\")\n        if role == \"system\":\n            continue\n\n        if last_role and role == last_role:\n            turns = format_messages(messages)\n            raise ValueError(\"Messages don't properly alternate user/assistant:\\n\\n\" + turns)\n\n        last_role = role\n        last_non_system_role = role\n\n    # Ensure last non-system message is from user\n    return last_non_system_role == \"user\"\n\n\ndef ensure_alternating_roles(messages):\n    \"\"\"Ensure messages alternate between 'assistant' and 'user' roles.\n\n    Inserts empty messages of the opposite role when consecutive messages\n    of the same role are found.\n\n    Args:\n        messages: List of message dictionaries with 'role' and 'content' keys.\n\n    Returns:","sourceCodeStart":2,"sourceCodeEnd":38,"githubUrl":"https://github.com/Aider-AI/aider/blob/5dc9490bb35f9729ef2c95d00a19ccd30c26339c/aider/sendchat.py#L2-L38","documentation":"sanity_check_messages in aider/sendchat.py walks the message list and raises ValueError whenever two consecutive non-system messages share the same role — the list must strictly alternate user/assistant (system messages may appear anywhere). The message dumps the full formatted transcript (format_messages) so you can see the exact turn where duplication occurs. This check runs when AIDER_SANITY_CHECK_TURNS is set (send_completion) and feeds guard logic for models that require strict alternation (e.g. DeepSeek R1 via ensure_alternating_roles).","triggerScenarios":"Sending a completion where messages = [user, user, ...] or [assistant, assistant, ...] consecutively (system msgs ignored), e.g. after manually constructing history, merging two user edits into one turn, or a bug in chat-history replay that emits back-to-back same-role messages. Under send_completion it fires when the env var AIDER_SANITY_CHECK_TURNS is set; role alternation matters unconditionally for strict-alternation models.","commonSituations":"Building custom message histories for aider's model layer; summarization producing a user summary message concatenated after an existing user message; adapting transcripts from other tools that allow consecutive user turns.","solutions":["Run ensure_alternating_roles(messages) (same module) before sending — it inserts empty opposite-role messages to repair the list.","Find the duplicated turn in the dumped transcript in the error message and merge or remove the redundant same-role message at your construction site.","If you only wanted the boolean, call it without AIDER_SANITY_CHECK_TURNS set; note the function returns True/False for the last-message check but raises for alternation violations."],"exampleFix":"# before\nmessages = [\n    {\"role\": \"user\", \"content\": \"hi\"},\n    {\"role\": \"user\", \"content\": \"add a test\"},  # raises\n]\nsend(messages)\n\n# after\nfrom aider.sendchat import ensure_alternating_roles\nmessages = ensure_alternating_roles(messages)  # inserts {\"role\":\"assistant\",\"content\":\"\"} between them\nsend(messages)","handlingStrategy":"validation","validationCode":"from aider.sendchat import ensure_alternating_roles, sanity_check_messages\n\ndef prep_messages(messages):\n    messages = ensure_alternating_roles(messages)  # repairs consecutive same-role msgs\n    ok = sanity_check_messages(messages)           # True if last non-system msg is user\n    if not ok:\n        messages.append({\"role\": \"user\", \"content\": \"continue\"})\n    return messages","typeGuard":"def messages_alternate(messages) -> bool:\n    last = None\n    for m in messages:\n        r = m.get(\"role\")\n        if r == \"system\":\n            continue\n        if r == last:\n            return False\n        last = r\n    return True","tryCatchPattern":"try:\n    sanity_check_messages(messages)\nexcept ValueError as e:\n    if \"don't properly alternate\" in str(e):\n        messages = ensure_alternating_roles(messages)  # auto-repair and retry\n        sanity_check_messages(messages)\n    else:\n        raise","preventionTips":["Always run ensure_alternating_roles before sending to strict-alternation models (e.g. DeepSeek R1).","Never concatenate two same-role messages when building history manually — merge their content instead.","The error dump prints the full transcript; the first duplicated pair is your construction bug."],"tags":["messages","validation","chat-history","llm","aider"],"backgroundTag":null,"analyzedSha":"5dc9490bb35f9729ef2c95d00a19ccd30c26339c","analyzedAt":"2026-08-15T05:40:10.498Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}