From 641c82df192d4b34adf81225cd537a28531d82d1 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:02:54 +0200 Subject: [PATCH] fix: automations, timers and sub-agents fail with "Message already exists" (#32177) Automation runs, timers and sub-agents stopped getting a reply after the shared chats rework: the chat shows an empty answer and the run fails with `409 Message already exists or has an invalid ID.` Each of them creates the empty answer before asking the model, and the backend began refusing to fill an answer that is already in the chat; the server-side API flow in the docs and API calls without a user message broke the same way. Such an empty answer is now filled in again when it belongs to the same user and answers the same message, and API calls without a user message work as before, saving the reply on its own as the docs describe. A firing timer or sub-agent also no longer cancels the chat's other timers that are set to stop when the user writes next. Answers that are already finished, or that belong to another user in a shared chat, are still refused. Fixes #32066 --- backend/open_webui/main.py | 10 ++-- backend/open_webui/models/chats.py | 73 ++++++++++++++++++------------ 2 files changed, 52 insertions(+), 31 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 96a85be50e..63f5fc6ccb 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1560,9 +1560,10 @@ async def chat_completion( for mid, message in turn['messages'].items() } await event_emitter({'type': 'chat:messages', 'data': turn}) - await emit_chat_list_event({**metadata, 'message_id': user_message['id']}, chat_id) + user_message_id = user_message.get('id') + await emit_chat_list_event({**metadata, 'message_id': user_message_id}, chat_id) for message_id, message in turn['messages'].items(): - if message_id != user_message['id'] and message.get('parentId') != user_message['id']: + if message_id != user_message_id and message.get('parentId') != user_message_id: continue await publish_event( request, @@ -1571,7 +1572,10 @@ async def chat_completion( subject_id=message_id, data={'chat_id': chat_id, 'role': message['role'], 'model': message.get('model')}, ) - if not getattr(request.state, 'internal', False): + if user_message_id and not ( + getattr(request.state, 'internal', False) + or (user_message.get('meta') or {}).get('internal') + ): try: from open_webui.utils.timers import cancel_timers_for_chat diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index cb2eac689f..cfc59bc0e7 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -1196,35 +1196,52 @@ class ChatTable: raise HTTPException(404, 'Chat not found.') history = (chat.chat or {}).get('history') or {'messages': {}, 'currentId': None} messages = history.get('messages') or {} - ids = [user_message.get('id'), *[entry.get('message_id') for entry in message_ids]] - if ( - not all(isinstance(mid, str) and mid for mid in ids) - or len(set(ids)) != len(ids) - or any(mid in messages for mid in ids[1:]) - ): + user_message_id = user_message.get('id') + assistant_ids = [entry.get('message_id') for entry in message_ids] + ids = [user_message_id, *assistant_ids] if user_message else assistant_ids + if not all(isinstance(mid, str) and mid for mid in ids) or len(set(ids)) != len(ids): raise HTTPException(409, 'Message already exists or has an invalid ID.') - existing = messages.get(ids[0]) - if existing and (existing.get('role') != 'user' or (existing.get('user_id') or chat.user_id) != user.id): - raise HTTPException(403, 'You can only regenerate your own messages.') - parent_id = existing.get('parentId') if existing else user_message.get('parentId') - if parent_id is not None and parent_id not in messages: - raise HTTPException(409, 'Parent message no longer exists.') - message = existing or ( - dict(user_message) - if chat.user_id == user.id - else {key: user_message[key] for key in ('content', 'files', 'models') if key in user_message} - ) - if not existing: - message.update( - id=ids[0], - parentId=parent_id, - role='user', - childrenIds=[], - timestamp=int(time.time()), - user_id=user.id, - user={'id': user.id, 'name': user.name}, + # An empty reply may be prepared before the request. + for mid in assistant_ids: + existing_reply = messages.get(mid) + if existing_reply and ( + existing_reply.get('role') != 'assistant' + or existing_reply.get('done') + or existing_reply.get('content') + or existing_reply.get('output') + or (existing_reply.get('user_id') or chat.user_id) != user.id + or (user_message and existing_reply.get('parentId') != user_message_id) + ): + raise HTTPException(409, 'Message already exists or has an invalid ID.') + turn = {} + parent_id = None + if user_message: + existing = messages.get(user_message_id) + if existing and ( + existing.get('role') != 'user' or (existing.get('user_id') or chat.user_id) != user.id + ): + raise HTTPException(403, 'You can only regenerate your own messages.') + parent_id = existing.get('parentId') if existing else user_message.get('parentId') + if parent_id is not None and parent_id not in messages: + raise HTTPException(409, 'Parent message no longer exists.') + message = existing or ( + dict(user_message) + if chat.user_id == user.id + else {key: user_message[key] for key in ('content', 'files', 'models') if key in user_message} + ) + if not existing: + message.update( + id=user_message_id, + parentId=parent_id, + role='user', + childrenIds=[], + timestamp=int(time.time()), + user_id=user.id, + user={'id': user.id, 'name': user.name}, + ) + turn[user_message_id] = self.upsert_message_to_history( + history, user_message_id, self._clean_null_bytes(message) ) - turn = {ids[0]: self.upsert_message_to_history(history, ids[0], self._clean_null_bytes(message))} for entry in message_ids: mid = entry['message_id'] turn[mid] = self.upsert_message_to_history( @@ -1232,7 +1249,7 @@ class ChatTable: mid, { 'id': mid, - 'parentId': ids[0], + 'parentId': user_message_id, 'childrenIds': [], 'role': 'assistant', 'content': '',