unslothai/unsloth · error · ResearchConflictError

Assistant message does not match this research run

Error message

Assistant message does not match this research run

What it means

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.

Source

Thrown at studio/backend/storage/research_runs_db.py:214

                # reply carries text/source parts that _update_assistant drops on completion,
                # so binding one silently overwrites an existing answer.
                existing_answer = any(
                    isinstance(part, dict)
                    and (
                        (part.get("type") == "text" and (part.get("text") or "").strip())
                        or part.get("type") == "source"
                    )
                    and part.get("researchRunId") is None
                    for part in _loads(message["content_json"], [])
                )
                if (
                    message["thread_id"] != thread_id
                    or message["role"] != "assistant"
                    or message["parent_id"] != user_message_id
                    or existing_run_id not in (None, run_id)
                    or (existing_run_id is None and existing_answer)
                ):
                    raise ResearchConflictError(
                        "Assistant message does not match this research run"
                    )
                merged_metadata = (
                    dict(existing_metadata) if isinstance(existing_metadata, dict) else {}
                )
                merged_metadata.update(metadata)
                conn.execute(
                    "UPDATE chat_messages SET metadata_json=? WHERE id=?",
                    (json.dumps(merged_metadata, ensure_ascii = False), assistant_message_id),
                )
        conn.execute(
            """
            INSERT INTO research_runs
                (id, owner_subject, thread_id, user_message_id, assistant_message_id,
                 status, config_json, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?, 'planning', ?, ?, ?)
            """,
            (

View on GitHub (pinned to 203007d190)

Solutions

  1. Always use the assistant message id returned by the create-run call for that exact run — never a cached one.
  2. Ensure the id points to the just-created empty assistant reply (role assistant, parent = the triggering user message).
  3. On conflict, re-fetch the thread's messages and restart the research run cleanly rather than rebinding.

Example fix

# before: rebinding an old message to a new run
run = create_research_run(thread_id, user_msg_id, assistant_message_id=old_assistant_id)

# after: create a fresh assistant message per run
new_msg = create_message(thread_id, role='assistant', parent_id=user_msg_id)
run = create_research_run(thread_id, user_msg_id, assistant_message_id=new_msg.id)
Defensive patterns

Strategy: validation

Validate before calling

# Verify the message is bindable BEFORE creating the run
msg = get_message(assistant_message_id)
assert msg is not None, 'message missing'
assert msg['thread_id'] == thread_id, 'wrong thread'
assert msg['role'] == 'assistant', 'not an assistant message'
assert msg['parent_id'] == user_message_id, 'wrong parent'
assert existing_run_id_of(msg) in (None, run_id), 'already bound'
assert not has_answer_parts(msg), 'message already carries an answer'
create_research_run(thread_id, user_message_id, assistant_message_id)

Type guard

def is_bindable_assistant_message(msg, thread_id: str, user_message_id: str) -> bool:
    return (
        msg is not None
        and msg['thread_id'] == thread_id
        and msg['role'] == 'assistant'
        and msg['parent_id'] == user_message_id
        and msg.get('researchRunId') is None
    )

Try / catch

try:
    bind_run_to_message(run_id, assistant_message_id)
except ResearchConflictError:
    # message is claimed/incompatible: create a FRESH assistant message and rebind
    fresh = create_empty_assistant_message(thread_id, parent_id=user_message_id)
    bind_run_to_message(run_id, fresh.id)

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/a940970a3e524d75. Report an issue: GitHub.