mirror of
https://github.com/open-webui/open-webui.git
synced 2026-07-17 08:21:12 -05:00
[PR #24617] refac: Prevent malformed chat history nodes when assistant placeholders are lost during concurrent saves #98781
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/open-webui/open-webui/pull/24617
Author: @Classic298
Created: 5/12/2026
Status: 🔄 Open
Base:
dev← Head:fix-chat-history📝 Commits (8)
22d84d2fix610e0b8fix: enforce graph invariants on message upsert, stop clobbering existing nodes0ac92e2fix: address 2nd review - presence-based merge, bidirectional edge, gated log1c2a713fix: harden parent-edge repair, detach dangling parentId, trim commentsb7bd853fix: address 3rd review - propagate parent to normalized row, drop destructive detach466883afix: address 4th review - don't clear normalized parent, guard malformed nodeed03275test: extract pure history-upsert helpers and add regression tests (#24758-style)824c961Revert "test: extract pure history-upsert helpers and add regression tests (#24758-style)"📊 Changes
2 files changed (+91 additions, -21 deletions)
View changed files
📝
backend/open_webui/models/chats.py(+63 -8)📝
backend/open_webui/utils/middleware.py(+28 -13)📄 Description
Summary
This PR hardens chat message persistence against a race condition where partial assistant-message updates can create malformed history nodes. It ensures that fallback upserts always create a valid Open WebUI message object and that final streaming/cancel saves include the required graph fields.
Background
Open WebUI stores chat history as a message graph inside
chat.chat.history.messages. Each message node is expected to contain structural fields such as:A valid graph is required for the frontend to reconstruct the visible conversation by walking backward from
history.currentIdthrough each message’sparentId.We observed a production chat export where the final assistant message existed under the correct message ID key, and the parent user message referenced it in
childrenIds, but the assistant node itself only contained partial response fields:It was missing
id,parentId,childrenIds, androle. This made the chat import successfully as JSON, but the Open WebUI frontend never finished loading the conversation because the history graph was semantically broken.Root Cause
Chats.upsert_message_to_chat_by_id_and_message_id()supports partial message updates. If the target message already exists, this is safe because it merges the partial update into the existing full message object:However, if the message does not exist, the old code stored the partial update as the complete message:
That is unsafe because several callers, especially in streaming response handling, intentionally pass only partial fields such as:
Those payloads assume that an assistant placeholder already exists.
If that placeholder is missing, the partial payload becomes a malformed full history node.
When This Can Happen
This can happen under concurrent async chat-save conditions, especially with long-running streaming/tool-call responses.
A typical sequence is:
chat:completion,status,source, or tool-related events.chat.chatJSON using read-modify-write.upsert_message_to_chat_by_id_and_message_id()with only partial fields likedone,content,output, andusage.This is most likely under these conditions:
chat.chatJSON field close together.sources, files, or output payloads, increasing update duration and race-window size.Why It Matters
Malformed message nodes are hard to diagnose because:
history.currentId, but the current message object is missing expected graph fields.A broken node also makes exports/imports fragile and can require manual JSON surgery to recover the chat.
Changes
This PR makes two defensive changes.
Chats.upsert_message_to_chat_by_id_and_message_id()now creates a valid message skeleton if the message ID does not already exist.Instead of storing a partial payload directly, it now ensures the fallback message has:
middleware.pynow pass complete assistant-message graph fields.Final assistant updates now include:
This means the final save is safe even if the placeholder was lost before the final update.
Why This Is Safe
The normal path is unchanged: if the assistant placeholder exists, the upsert still merges into it.
The fallback path is now safer: if the placeholder is missing, the newly created message is at least structurally valid and can be traversed by the frontend.
Existing fields from the provided message payload still win because the skeleton is merged before
**message.Limitations
This PR does not fully eliminate all possible lost-update races on the whole
chat.chatJSON field. A more complete long-term fix would be to avoid whole-document read-modify-write for per-message updates, for example by:chat_messagetable the source of truth and rebuildingchat.chat.historyfrom it.However, this PR prevents the dangerous failure mode where a partial update becomes an invalid message node.
Validation
Validated by:
id,parentId,childrenIds, androle.Expected Outcome
After this change, even if concurrent chat updates temporarily lose an assistant placeholder, subsequent final/partial upserts will not persist a malformed message object. The chat history graph remains loadable and structurally valid.
Contributor License Agreement
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.