{"record":{"id":"d51055ba41ebfc9e","repo":"bytedance/deer-flow","slug":"failed-to-update-thread","errorCode":null,"errorMessage":"Failed to update thread","messagePattern":"Failed to update thread","errorType":"http","errorClass":"HTTPException","httpStatus":500,"severity":"error","filePath":"backend/app/gateway/routers/threads.py","lineNumber":1009,"sourceCode":"async def patch_thread(thread_id: ThreadId, body: ThreadPatchRequest, request: Request) -> ThreadResponse:\n    \"\"\"Merge metadata into a thread record.\"\"\"\n    from app.gateway.deps import get_thread_store\n\n    thread_store = get_thread_store(request)\n    record = await thread_store.get(thread_id)\n    if record is None:\n        raise HTTPException(status_code=404, detail=f\"Thread {thread_id} not found\")\n\n    # ``body.metadata`` already stripped by ``ThreadPatchRequest._strip_reserved``.\n    # Pin/unpin is not conversation activity, so it must not bump ``updated_at``.\n    # Other metadata PATCH callers keep the public endpoint's existing recency\n    # contract unless they get their own explicit no-touch API surface.\n    touch = not _is_pin_metadata_patch(body.metadata)\n    try:\n        await thread_store.update_metadata(thread_id, body.metadata, touch=touch)\n    except Exception:\n        logger.exception(\"Failed to patch thread %s\", sanitize_log_param(thread_id))\n        raise HTTPException(status_code=500, detail=\"Failed to update thread\")\n\n    # Re-read to get the merged metadata and the store's timestamp decision.\n    record = await thread_store.get(thread_id) or record\n    return ThreadResponse(\n        thread_id=thread_id,\n        status=record.get(\"status\", \"idle\"),\n        created_at=coerce_iso(record.get(\"created_at\", \"\")),\n        updated_at=coerce_iso(record.get(\"updated_at\", \"\")),\n        metadata=record.get(\"metadata\", {}),\n    )\n\n\n@router.get(\"/{thread_id}\", response_model=ThreadResponse)\n@require_permission(\"threads\", \"read\", owner_check=True)\nasync def get_thread(thread_id: ThreadId, request: Request) -> ThreadResponse:\n    \"\"\"Get thread info from metadata plus the graph's materialized state.\"\"\"\n    from app.gateway.deps import get_thread_store\n","sourceCodeStart":991,"sourceCodeEnd":1027,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/app/gateway/routers/threads.py#L991-L1027","documentation":"500 from PATCH /threads/{thread_id} when thread_store.update_metadata(thread_id, body.metadata, touch=touch) raises — the merge write to the thread store failed after the record was read. Logged as 'Failed to patch thread %s' with the full traceback.","triggerScenarios":"PATCH while the SQL store fails on UPDATE: connection loss, lock timeout from a concurrent writer, constraint violation from the merged metadata (e.g. oversized value), or schema drift.","commonSituations":"Database outage or failover mid-request; two clients PATCHing the same thread with row-lock contention; metadata values exceeding column size after merge; migrations not applied.","solutions":["Check the Gateway log traceback for the underlying SQL error.","Verify DB health and apply pending migrations.","Keep PATCH metadata small and flat; split large updates into several PATCHes.","Retry once the concurrent writer or lock holder finishes."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { await api.patch(`/api/threads/${id}`, {metadata}); }\ncatch (err) { if (err.status === 500) { await backoff(); retryOnce(); } else throw err; }","preventionTips":["Keep PATCH bodies small; split large metadata updates.","Serialize concurrent PATCHes to the same thread client-side to avoid row-lock contention."],"tags":["threads","database","http-500","patch","metadata"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}