fix: chat corruption (openweb-ui#26668)#26971
Conversation
|
thanks for the video! |
|
LGTM! |
ErenAta16
left a comment
There was a problem hiding this comment.
The two new guards in upsert_message_to_chat_by_id_and_message_id (bail out to a plain re-fetch when parent_id is None but other messages already exist, or when parent_id points at something not in the map) both look like reasonable defenses against building a message tree with a dangling parent reference, which is exactly the shape of corruption that would make the chat unrenderable client-side afterward. The currentId guard is the same idea applied to the pointer itself — do not park currentId on a node whose own parent link is broken.
The stopResponse wiring in Messages.svelte is a good catch on its own: without it, deleting the in-flight active assistant message would leave the stream still writing into a message id that no longer exists in history.messages, which is a plausible route into the exact kind of corruption this PR is fixing structurally on the backend. Threading it through as a prop with a no-op default keeps existing callers that do not pass it unaffected.
open-webui#26668) Deleting a user message while its reply was still streaming corrupted the chat: after the generation finished, only the streamed reply survived on reload and the rest of the history became unreachable. Root cause: deleting a message did not cancel the in-flight generation. The backend task kept running and, on completion, upserted the deleted assistant message with no resolvable parent — recreating it as an orphan root and pointing currentId at it. createMessagesList walks parentId from currentId, so the broken parent chain stranded the prior conversation. Three fixes: 1. Frontend (Messages.svelte): when the message being deleted (or one of its reply children) is the not-yet-done assistant reply at currentId, call stopResponse(false) first to cancel the backend task. Gate on the message done-state (the codebase's own isGenerating heuristic) rather than the `generating` flag, which is only set in the MoA merge path. stopResponse is threaded through from Chat.svelte as a prop. 2. Backend (chats.py upsert_message_to_chat_by_id_and_message_id): if a stale completion upsert arrives for a message that is no longer in the chat with no resolvable parent (parent deleted, or no surviving message references it in childrenIds), skip the write instead of resurrecting it as an orphan root. Normal flow is unaffected — the user message is persisted before generation, so the assistant upsert resolves a live parent or hits the existing-message merge branch. 3. Backend: only advance currentId to the upserted message when its parentId is None or exists in the messages map, so currentId can never point at a message with a broken parent chain.
|
I rebased my PR |
Pull Request Checklist
Note to first-time contributors: Please open a discussion post in Discussions to discuss your idea/fix with the community before creating a pull request, and describe your changes before submitting a pull request.
This is to ensure large feature PRs are discussed with the community first, before starting work on it. If the community does not want this feature or it is not relevant for Open WebUI as a project, it can be identified in the discussion before working on the feature and submitting the PR.
Before submitting, make sure you've checked the following:
Closes #___/Relates to #___. If one does not exist, create one first. PRs without a linked issue or discussion may be closed without review.devbranch. PRs targetingmainwill be immediately closed.dev, and contains no unrelated commits.Changelog Entry
Description
Deleting a user message while its reply was still streaming corrupted the chat: after the generation finished, only the streamed reply survived on reload and the rest of the history became unreachable.
Root cause: deleting a message did not cancel the in-flight generation. The backend task kept running and, on completion, upserted the deleted assistant message with no resolvable parent — recreating it as an orphan root and pointing currentId at it. createMessagesList walks parentId from currentId, so the broken parent chain stranded the prior conversation.
Three fixes:
Frontend (Messages.svelte): when the message being deleted (or one of its reply children) is the not-yet-done assistant reply at currentId, call stopResponse(false) first to cancel the backend task. Gate on the message done-state (the codebase's own isGenerating heuristic) rather than the
generatingflag, which is only set in the MoA merge path. stopResponse is threaded through from Chat.svelte as a prop.Backend (chats.py upsert_message_to_chat_by_id_and_message_id): if a stale completion upsert arrives for a message that is no longer in the chat with no resolvable parent (parent deleted, or no surviving message references it in childrenIds), skip the write instead of resurrecting it as an orphan root. Normal flow is unaffected — the user message is persisted before generation, so the assistant upsert resolves a live parent or hits the existing-message merge branch.
Backend: only advance currentId to the upserted message when its parentId is None or exists in the messages map, so currentId can never point at a message with a broken parent chain.
Fixed
Additional Information
Screenshots or Videos
Screen.Recording.2026-07-10.102926.mp4
Contributor License Agreement
Note
Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.