bytedance/deer-flow · warning · HTTPException
Only the latest completed user turn can be edited
Error message
Only the latest completed user turn can be edited
What it means
Raised by _latest_editable_turn (HTTP 409) during edit-regenerate when the supplied human_message_id does not match the id of the most recent visible human message in the checkpoint's message list. The edit flow only permits rewriting the last completed user turn; anything older is refused.
Source
Thrown at backend/app/gateway/routers/thread_runs.py:442
def _is_terminal_assistant_text_message(message: Any) -> bool:
return _is_visible_ai_message(message) and bool(_message_text(message).strip()) and not _message_tool_calls(message)
def _has_title(values: dict[str, Any]) -> bool:
title = values.get("title")
return isinstance(title, str) and bool(title)
def _has_active_goal(snapshot: Any) -> bool:
goal = _checkpoint_values(snapshot).get("goal")
return isinstance(goal, dict) and goal.get("status") == "active"
def _latest_editable_turn(messages: list[Any], human_message_id: str) -> tuple[int, Any, int, Any, list[str]]:
latest_human_index = next((index for index in range(len(messages) - 1, -1, -1) if _is_visible_human_message(messages[index])), None)
if latest_human_index is None or _message_id(messages[latest_human_index]) != human_message_id:
raise HTTPException(status_code=409, detail="Only the latest completed user turn can be edited")
source_human = messages[latest_human_index]
last_ai_index: int | None = None
for index, message in enumerate(messages[latest_human_index + 1 :], start=latest_human_index + 1):
if _is_visible_human_message(message):
break
if _is_visible_ai_message(message):
last_ai_index = index
if last_ai_index is None or not _is_terminal_assistant_text_message(messages[last_ai_index]):
raise HTTPException(status_code=409, detail="Only completed assistant text turns can be edited")
source_message_ids = [message_id for message in messages[latest_human_index : last_ai_index + 1] if (message_id := _message_id(message))]
return latest_human_index, source_human, last_ai_index, messages[last_ai_index], source_message_ids
def _event_message_id(row: dict[str, Any]) -> str | None:
content = row.get("content")View on GitHub (pinned to 1dd6ba1acb)
Solutions
- Refetch the thread state and use the id of the latest visible human message from the current checkpoint.
- Ensure only one client operates on the thread at a time, or refresh the thread view before initiating an edit.
- Confirm the message id you pass is the server-side id (from the checkpointed messages), not a client-local placeholder id.
- After a regenerate, use the newly stamped message ids rather than cached ones.
Example fix
// before
await editPrepare(threadId, oldHumanMessageId, newText);
// after
const {messages} = await getThreadState(threadId);
const lastHuman = [...messages].reverse().find(m => m.type === 'human');
if (lastHuman.id !== oldHumanMessageId) oldHumanMessageId = lastHuman.id;
await editPrepare(threadId, oldHumanMessageId, newText); Defensive patterns
Strategy: validation
Validate before calling
const {messages} = await getThreadState(threadId);
const lastHuman = [...messages].reverse().find(m => m.type === 'human' && !m.hidden);
if (lastHuman?.id !== requestedHumanId) {
// refresh UI to the latest turn instead of editing
} Type guard
function isLatestVisibleHuman(messages: Msg[], id: string): boolean {
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i];
if (m.type === 'human' && !m.hidden) return m.id === id;
}
return false;
} Try / catch
try { await editPrepare(threadId, humanId, text); } catch (e) { if (e.status === 409 && /latest completed user turn/.test(e.detail)) { await refreshThread(threadId); } else throw e; } Prevention
- Refresh thread state right before edit
- Disable Edit on all but the latest user turn
- Use server-assigned message ids
When it happens
Trigger: POST to the edit/prepare endpoint with a human_message_id from an earlier turn while newer turns exist; the UI sends a stale message id after the thread advanced (e.g. another tab or device sent a message); the last human message is hidden from UI (hide_from_ui) so the id supplied is not the one the server considers 'latest visible'.
Common situations: Stale client state after a run finished in another session; message id confusion between client-generated and server-stamped ids; trying to edit a turn that was already regenerated (its message got a new id).
Related errors
- Only completed assistant text turns can be edited
- Checkpoint is missing checkpoint_id
- Could not find source run for assistant message
- Could not safely resolve the checkpoint before the target us
- Could not find an addressable checkpoint before the target u
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/54db2e74e6804d4d.
Report an issue: GitHub.