bytedance/deer-flow · warning · HTTPException

Thread {thread_id} has no checkpoint

Error message

Thread {thread_id} has no checkpoint

What it means

Raised in _prepare_regenerate_payload (HTTP 404) when the thread's latest checkpoint exists as a tuple but its configurable carries no checkpoint_id. Practically: the thread has no addressable checkpoint state (no completed graph step), so there is nothing to regenerate from.

Source

Thrown at backend/app/gateway/routers/thread_runs.py:640

    if record is None:
        return None
    if getattr(record, "thread_id", None) != thread_id:
        return None
    if _run_status_value(record) != RunStatus.interrupted.value:
        return None
    return source_run_id


async def _prepare_regenerate_payload(thread_id: str, message_id: str, request: Request) -> RegeneratePrepareResponse:
    accessor, latest_config = await build_thread_checkpoint_state_accessor(request, thread_id=thread_id)
    try:
        latest_checkpoint = await accessor.aget(latest_config)
    except Exception as exc:
        logger.exception("Failed to read latest checkpoint for regenerate thread %s", thread_id)
        raise HTTPException(status_code=500, detail="Failed to read latest checkpoint") from exc
    latest_checkpoint_id = _checkpoint_configurable(latest_checkpoint).get("checkpoint_id")
    if not latest_checkpoint_id:
        raise HTTPException(status_code=404, detail=f"Thread {thread_id} has no checkpoint")

    messages = _checkpoint_messages(latest_checkpoint)
    target_index = next((i for i, message in enumerate(messages) if _message_id(message) == message_id), None)
    if target_index is None:
        # A response interrupted during an LLM call can be visible in the live
        # stream without ever reaching a checkpoint. The server-stamped run ID
        # on the latest user message is the durable link to that partial turn.
        previous_human = next(
            (message for message in reversed(messages) if _is_visible_human_message(message)),
            None,
        )
        target_run_id = await _find_interrupted_target_run_id(thread_id, previous_human, request) if previous_human is not None else None
        if target_run_id is None:
            raise HTTPException(status_code=404, detail=f"Message {message_id} not found")
    else:
        target_message = messages[target_index]
        if not _is_visible_ai_message(target_message):
            raise HTTPException(status_code=409, detail="Only visible assistant messages can be regenerated")

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Send at least one message and let a run complete before offering regenerate.
  2. Confirm via GET thread state that a checkpoint_id is present.
  3. If history was wiped intentionally, treat the thread as new and start a fresh run.

Example fix

// before
regeneratePrepare(threadId, messageId);
// after
const st = await getThreadState(threadId);
if (!st.checkpoint_id) throw new Error('Nothing to regenerate yet');
regeneratePrepare(threadId, messageId);
Defensive patterns

Strategy: validation

Validate before calling

const st = await getThreadState(threadId);
if (!st?.checkpoint_id) { notify('Send a message first'); return; }

Type guard

function threadHasCheckpoint(st: {checkpoint_id?: string | null}): boolean {
  return typeof st.checkpoint_id === 'string' && st.checkpoint_id.length > 0;
}

Try / catch

try { await regeneratePrepare(threadId, messageId); } catch (e) { if (e.status === 404 && /has no checkpoint/.test(e.detail)) { hideRegenerateButton(); } else throw e; }

Prevention

When it happens

Trigger: Regenerate-prepare on a brand-new thread with zero runs; a run that was created but crashed before its first checkpoint write; checkpoint store emptied for the thread.

Common situations: UI shows a regenerate button before the first response; user deleted thread history; run interrupted pre-checkpoint.

Related errors


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