diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index e1f40dd168..56098a9ae3 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -584,13 +584,15 @@ class ChatTable: return node existing = messages.get(message_id) - if existing is not None: + if isinstance(existing, dict): # {**existing, **message}: omitted keys keep existing, explicit # values (incl. parentId=None, childrenIds=[]) win. messages[message_id] = enforce_node_invariants({**existing, **message}) else: - # Node missing: a concurrent whole-chat write likely dropped the - # placeholder. Warn only when the payload is itself partial. + if existing is not None: + log.warning(f'upsert: discarding malformed existing node {message_id}') + # Node missing/unusable: a concurrent whole-chat write likely + # dropped the placeholder. Warn only when the payload is partial. is_partial = not all(k in message for k in ('id', 'parentId', 'childrenIds', 'role')) if is_partial: log.warning( @@ -623,16 +625,18 @@ class ChatTable: chat['history'] = history - # Dual-write to chat_message table. Alias camelCase parentId to - # snake_case so the normalized row's parent link is repaired too - # (callee gates its update on 'parent_id'). + # Send parent_id only when known; parent_id=None would clear a real + # link on the normalized row from a possibly-stale snapshot. node = history['messages'][message_id] + cm_data = {**node} + if node.get('parentId') is not None: + cm_data['parent_id'] = node['parentId'] try: await ChatMessages.upsert_message( message_id=message_id, chat_id=id, user_id=user_id, - data={**node, 'parent_id': node.get('parentId')}, + data=cm_data, ) except Exception as e: log.warning(f'Failed to write to chat_message table: {e}') diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index f352295a39..14cfe4d16c 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -3583,13 +3583,12 @@ async def streaming_chat_response_handler(response, ctx): model_id = form_data.get('model', '') def build_assistant_message_update(**fields): - # Only placeholder-repair fields, and each only when authoritative, - # so the upsert merge can't clobber an existing node (chats.py - # synthesizes what's missing). + # Placeholder-repair fields only, each when authoritative; + # structural keys last so callers can't override them. update = { + **fields, 'id': metadata['message_id'], 'role': 'assistant', - **fields, } if model_id: update['model'] = model_id