{"record":{"id":"9ee31c61934efbcc","repo":"unslothai/unsloth","slug":"this-thread-already-has-a-deep-research-run-9ee31c","errorCode":null,"errorMessage":"This thread already has a Deep Research run","messagePattern":"This thread already has a Deep Research run","errorType":"exception","errorClass":"ResearchConflictError","httpStatus":409,"severity":"error","filePath":"studio/backend/storage/research_runs_db.py","lineNumber":158,"sourceCode":"    created_at: int | None = None,\n) -> dict:\n    created = created_at or now_ms()\n    conn = get_connection()\n    try:\n        conn.execute(\"BEGIN IMMEDIATE\")\n        try:\n            conn.execute(\n                \"INSERT INTO research_thread_claims (owner_subject, thread_id, created_at) \"\n                \"VALUES (?, ?, ?)\",\n                (owner_subject, thread_id, created),\n            )\n        except sqlite3.IntegrityError as exc:\n            claim = conn.execute(\n                \"SELECT 1 FROM research_thread_claims WHERE thread_id=?\",\n                (thread_id,),\n            ).fetchone()\n            if claim is not None:\n                raise ResearchConflictError(\"This thread already has a Deep Research run\") from exc\n            raise\n        if assistant_message_id:\n            message = conn.execute(\n                \"SELECT * FROM chat_messages WHERE id=?\", (assistant_message_id,)\n            ).fetchone()\n            metadata = {\n                \"researchRunId\": run_id,\n                \"researchStatus\": \"planning\",\n                \"researchPlanRevision\": 0,\n                \"serverManaged\": True,\n            }\n            if message is None:\n                conn.execute(\n                    \"\"\"INSERT INTO chat_messages\n                       (id, thread_id, parent_id, role, content_json, metadata_json, created_at)\n                       VALUES (?, ?, ?, 'assistant', '[]', ?, ?)\"\"\",\n                    (\n                        assistant_message_id,","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/storage/research_runs_db.py#L140-L176","documentation":"ResearchConflictError raised while creating a Deep Research run: inserting into research_thread_claims hit sqlite3.IntegrityError, and a follow-up SELECT confirmed a claim row already exists for that thread_id — the table enforces one active research run per thread (unique thread_id). The insert+check pattern distinguishes this from an unrelated integrity failure (which re-raises bare).","triggerScenarios":"Two 'start research' requests for the same chat thread racing (double-click, retry, or two tabs); starting a new run while a previous run is still active/unfinished in that thread; a client that does not wait for the terminal state before issuing the next run.","commonSituations":"Double-submit from a flaky network retry; UI lets the user click Start again before the run reaches a terminal state; background poller that re-triggers creation.","solutions":["Wait for the existing run to finish (or cancel it) before starting another in the same thread.","Disable the Start button while a run is active; key it off the thread's researchStatus metadata.","On catching this conflict, re-fetch the thread state and surface the existing run instead of erroring to the user."],"exampleFix":"// before\nbutton.onclick = () => api.startResearch(threadId); // double-click => conflict\n\n// after\nif (thread.researchStatus && !isTerminal(thread.researchStatus)) return;\nbutton.disabled = true;\ntry { await api.startResearch(threadId); } finally { button.disabled = false; }","handlingStrategy":"validation","validationCode":"// Only start a run when the thread has none active\nconst status = thread.metadata?.researchStatus;\nconst terminal = ['completed', 'failed', 'cancelled'].includes(status);\nif (status && !terminal) throw new ConflictError('This thread already has a Deep Research run');\nawait api.startResearch(threadId);","typeGuard":"function threadHasActiveRun(t: Thread): boolean {\n  const s = t.metadata?.researchStatus;\n  return !!s && !['completed', 'failed', 'cancelled'].includes(s);\n}","tryCatchPattern":"try { await createRun(threadId, ...); }\ncatch (e) {\n  if (e?.name === 'ResearchConflictError' || /already has a Deep Research run/.test(e?.message)) {\n    const state = await fetchThreadState(threadId);  // adopt the live run\n    showExistingRun(state.activeRun);\n  } else throw e;\n}","preventionTips":["Disable the Start control while researchStatus is non-terminal","Make run creation idempotent per thread via a client request id","On network retry, check for the existing run before re-submitting"],"tags":["concurrency","unique-constraint","deep-research","state-machine","double-submit"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}