bytedance/deer-flow · warning · HTTPException

Thread {thread_id} not found

Error message

Thread {thread_id} not found

What it means

404 from POST /api/threads/{thread_id}/branches when thread_store.get(thread_id) returns None — no thread_meta record exists for the source thread. The endpoint requires a persisted main-conversation thread before it can branch from one of its turns.

Source

Thrown at backend/app/gateway/routers/threads.py:796

        thread_id=thread_id,
        status="idle",
        created_at=now,
        updated_at=now,
        metadata=body.metadata,
    )


@router.post("/{thread_id}/branches", response_model=ThreadBranchResponse)
@require_permission("threads", "write", owner_check=True, require_existing=True)
async def branch_thread(thread_id: ThreadId, body: ThreadBranchRequest, request: Request) -> ThreadBranchResponse:
    """Create a new main-thread branch from a completed assistant turn."""
    from app.gateway.deps import get_thread_store

    thread_store = get_thread_store(request)

    source_record = await thread_store.get(thread_id)
    if source_record is None:
        raise HTTPException(status_code=404, detail=f"Thread {thread_id} not found")

    source_metadata = source_record.get("metadata") or {}
    if source_metadata.get(_SIDECAR_METADATA_KEY) is True:
        raise HTTPException(status_code=409, detail="Branching is only available in the main conversation.")
    source_accessor, source_config = build_checkpoint_state_accessor(
        request,
        thread_id=thread_id,
        assistant_id=source_record.get("assistant_id"),
    )

    target_message_ids = {body.message_id, *body.message_ids}
    snapshot = await _find_branch_checkpoint(source_accessor, source_config, target_message_ids)
    parent_checkpoint_id = _checkpoint_id(snapshot)
    if not parent_checkpoint_id:
        raise HTTPException(status_code=409, detail="This turn can no longer be branched from.")
    target_human = _branch_target_human_message(_checkpoint_messages(snapshot), target_message_ids)
    target_human_id = _message_id(target_human)
    if not target_human_id:

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Confirm the thread exists: GET /api/threads/{thread_id} returns 200 before branching.
  2. If it was deleted, re-create it or branch from an existing thread.
  3. Check you are authenticated as the thread's owner user when the store is user-scoped.
  4. Regenerate/repair the client's thread list so it only offers branchable ids.

Example fix

// before
await fetch(`/api/threads/${staleId}/branches`, {method: 'POST', body: JSON.stringify({message_id: id})});
// after
const exists = await fetch(`/api/threads/${staleId}`).ok;
if (!exists) { /* refresh thread list, pick a live thread */ }
await fetch(`/api/threads/${staleId}/branches`, {method: 'POST', body: JSON.stringify({message_id: id})});
Defensive patterns

Strategy: validation

Validate before calling

const res = await fetch(`/api/threads/${id}`);
if (res.status === 404) { /* drop branch affordance; refresh thread list */ }

Try / catch

try { await api.post(`/api/threads/${id}/branches`, {message_id}); }
catch (err) { if (err.status === 404) removeThreadFromUI(id); else throw err; }

Prevention

When it happens

Trigger: Branching a thread_id that was never created via POST /threads, was deleted, belongs to another owner (store scoped by user), or exists only as checkpoints with no thread_meta row.

Common situations: Frontend keeps a stale thread id after the thread was deleted or its ownership changed; calling the branch API against a sidecar/generated thread that never got a meta record; typos or truncated UUIDs in the URL.

Related errors


AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14). Data as JSON: /api/errors/07a0c97c5f13b4d7. Report an issue: GitHub.