{"record":{"id":"5a6bf8f40b6072ce","repo":"bytedance/deer-flow","slug":"only-completed-assistant-text-turns-can-be-edited","errorCode":null,"errorMessage":"Only completed assistant text turns can be edited","messagePattern":"Only completed assistant text turns can be edited","errorType":"http","errorClass":"HTTPException","httpStatus":409,"severity":"warning","filePath":"backend/app/gateway/routers/thread_runs.py","lineNumber":453,"sourceCode":"    goal = _checkpoint_values(snapshot).get(\"goal\")\n    return isinstance(goal, dict) and goal.get(\"status\") == \"active\"\n\n\ndef _latest_editable_turn(messages: list[Any], human_message_id: str) -> tuple[int, Any, int, Any, list[str]]:\n    latest_human_index = next((index for index in range(len(messages) - 1, -1, -1) if _is_visible_human_message(messages[index])), None)\n    if latest_human_index is None or _message_id(messages[latest_human_index]) != human_message_id:\n        raise HTTPException(status_code=409, detail=\"Only the latest completed user turn can be edited\")\n\n    source_human = messages[latest_human_index]\n    last_ai_index: int | None = None\n    for index, message in enumerate(messages[latest_human_index + 1 :], start=latest_human_index + 1):\n        if _is_visible_human_message(message):\n            break\n        if _is_visible_ai_message(message):\n            last_ai_index = index\n\n    if last_ai_index is None or not _is_terminal_assistant_text_message(messages[last_ai_index]):\n        raise HTTPException(status_code=409, detail=\"Only completed assistant text turns can be edited\")\n\n    source_message_ids = [message_id for message in messages[latest_human_index : last_ai_index + 1] if (message_id := _message_id(message))]\n    return latest_human_index, source_human, last_ai_index, messages[last_ai_index], source_message_ids\n\n\ndef _event_message_id(row: dict[str, Any]) -> str | None:\n    content = row.get(\"content\")\n    if isinstance(content, BaseMessage):\n        return _message_id(content)\n    if isinstance(content, dict):\n        return _message_id(content)\n    return None\n\n\ndef _run_last_ai_matches_message(record: RunRecord, message: Any) -> bool:\n    last_ai_message = (record.last_ai_message or \"\").strip()\n    if not last_ai_message:\n        return False","sourceCodeStart":435,"sourceCodeEnd":471,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/app/gateway/routers/thread_runs.py#L435-L471","documentation":"Raised by _latest_editable_turn (HTTP 409) when, after the target human message, there is no visible AI message that is a terminal assistant text message. Editing requires a completed assistant text turn to anchor the replay; tool-call stubs, interrupted responses, or non-text outputs do not qualify.","triggerScenarios":"Editing a turn whose assistant response is still streaming or was interrupted mid-LLM-call; the turn ended with only tool-call/AI messages that are not terminal text; the last AI message was filtered out as hidden.","commonSituations":"User clicks Edit while a run is still in flight; the previous run errored before emitting a final text message; assistant response consisted only of artifacts/tool output.","solutions":["Wait for the current run to reach a terminal state (success) before invoking edit-regenerate.","Check the run status via the runs endpoint and only enable the Edit affordance for completed turns with text content.","If the last run failed, regenerate it first so a completed assistant text message exists, then edit.","Verify _is_terminal_assistant_text_message criteria are met (plain text content, no pending tool_calls) for the target turn."],"exampleFix":"// before\nawait editPrepare(threadId, humanId, newText); // fired right after send\n// after\nawait waitForRunCompletion(threadId, runId);\nconst state = await getThreadState(threadId);\nconst lastAi = [...state.messages].reverse().find(m => m.type === 'ai' && !m.tool_calls);\nif (lastAi) await editPrepare(threadId, humanId, newText);","handlingStrategy":"validation","validationCode":"const run = await getLatestRun(threadId);\nif (run.status !== 'success') return; // no completed assistant turn to edit yet","typeGuard":"function isCompletedAssistantTurn(msg: Msg | undefined): msg is Msg {\n  return !!msg && msg.type === 'ai' && !msg.hidden && !msg.tool_calls && typeof msg.content === 'string' && msg.content.length > 0;\n}","tryCatchPattern":"try { await editPrepare(threadId, humanId, text); } catch (e) { if (e.status === 409 && /completed assistant text/.test(e.detail)) { notify('Wait for the response to finish before editing'); } else throw e; }","preventionTips":["Gate Edit behind run completion","Treat interrupted runs as non-editable","Re-check terminal AI message presence after streaming ends"],"tags":["edit","regenerate","incomplete-turn","http-409"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}