{"record":{"id":"a940970a3e524d75","repo":"unslothai/unsloth","slug":"assistant-message-does-not-match-this-research-run","errorCode":null,"errorMessage":"Assistant message does not match this research run","messagePattern":"Assistant message does not match this research run","errorType":"exception","errorClass":"ResearchConflictError","httpStatus":409,"severity":"error","filePath":"studio/backend/storage/research_runs_db.py","lineNumber":214,"sourceCode":"                # reply carries text/source parts that _update_assistant drops on completion,\n                # so binding one silently overwrites an existing answer.\n                existing_answer = any(\n                    isinstance(part, dict)\n                    and (\n                        (part.get(\"type\") == \"text\" and (part.get(\"text\") or \"\").strip())\n                        or part.get(\"type\") == \"source\"\n                    )\n                    and part.get(\"researchRunId\") is None\n                    for part in _loads(message[\"content_json\"], [])\n                )\n                if (\n                    message[\"thread_id\"] != thread_id\n                    or message[\"role\"] != \"assistant\"\n                    or message[\"parent_id\"] != user_message_id\n                    or existing_run_id not in (None, run_id)\n                    or (existing_run_id is None and existing_answer)\n                ):\n                    raise ResearchConflictError(\n                        \"Assistant message does not match this research run\"\n                    )\n                merged_metadata = (\n                    dict(existing_metadata) if isinstance(existing_metadata, dict) else {}\n                )\n                merged_metadata.update(metadata)\n                conn.execute(\n                    \"UPDATE chat_messages SET metadata_json=? WHERE id=?\",\n                    (json.dumps(merged_metadata, ensure_ascii = False), assistant_message_id),\n                )\n        conn.execute(\n            \"\"\"\n            INSERT INTO research_runs\n                (id, owner_subject, thread_id, user_message_id, assistant_message_id,\n                 status, config_json, created_at, updated_at)\n            VALUES (?, ?, ?, ?, ?, 'planning', ?, ?, ?)\n            \"\"\",\n            (","sourceCodeStart":196,"sourceCodeEnd":232,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/storage/research_runs_db.py#L196-L232","documentation":"ResearchConflictError raised while binding a research run to its assistant chat message: the UPDATE precondition failed — the target message exists but does not match the run. The guard requires the message's thread_id to equal the run's thread, role == 'assistant', parent_id == the run's user message, no prior run id (or the same one), and (when unclaimed) no existing answer/source parts. Any mismatch aborts so a run can never hijack or overwrite another run's message.","triggerScenarios":"Passing an assistant_message_id from a different thread; the id belonging to a user-role message; the message already bound to another researchRunId; the message already carries answer/source parts tagged with another run; parent chain changed because the thread was edited/forked.","commonSituations":"Client reuses a cached message id after the thread was forked or re-created; retry logic pairing a NEW run with the OLD run's assistant message; concurrent runs in one thread racing to claim the same message.","solutions":["Always use the assistant message id returned by the create-run call for that exact run — never a cached one.","Ensure the id points to the just-created empty assistant reply (role assistant, parent = the triggering user message).","On conflict, re-fetch the thread's messages and restart the research run cleanly rather than rebinding."],"exampleFix":"# before: rebinding an old message to a new run\nrun = create_research_run(thread_id, user_msg_id, assistant_message_id=old_assistant_id)\n\n# after: create a fresh assistant message per run\nnew_msg = create_message(thread_id, role='assistant', parent_id=user_msg_id)\nrun = create_research_run(thread_id, user_msg_id, assistant_message_id=new_msg.id)","handlingStrategy":"validation","validationCode":"# Verify the message is bindable BEFORE creating the run\nmsg = get_message(assistant_message_id)\nassert msg is not None, 'message missing'\nassert msg['thread_id'] == thread_id, 'wrong thread'\nassert msg['role'] == 'assistant', 'not an assistant message'\nassert msg['parent_id'] == user_message_id, 'wrong parent'\nassert existing_run_id_of(msg) in (None, run_id), 'already bound'\nassert not has_answer_parts(msg), 'message already carries an answer'\ncreate_research_run(thread_id, user_message_id, assistant_message_id)","typeGuard":"def is_bindable_assistant_message(msg, thread_id: str, user_message_id: str) -> bool:\n    return (\n        msg is not None\n        and msg['thread_id'] == thread_id\n        and msg['role'] == 'assistant'\n        and msg['parent_id'] == user_message_id\n        and msg.get('researchRunId') is None\n    )","tryCatchPattern":"try:\n    bind_run_to_message(run_id, assistant_message_id)\nexcept ResearchConflictError:\n    # message is claimed/incompatible: create a FRESH assistant message and rebind\n    fresh = create_empty_assistant_message(thread_id, parent_id=user_message_id)\n    bind_run_to_message(run_id, fresh.id)","preventionTips":["Create a new empty assistant message per run; never reuse ids across runs","Take message ids from the create-run response, not local cache, after forks/edits","On retry paths, re-derive both user and assistant message ids from server state"],"tags":["deep-research","precondition","message-binding","state-machine","concurrency"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}