From 939471cb3bf8c74603f6a331e8a95c280568fc64 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Fri, 6 Mar 2026 14:07:02 +0400 Subject: [PATCH] fix: Prevent duplicate conversations in conversation list (#13713) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agents using API channel inboxes (e.g., WhatsApp Automate) reported seeing the same conversation appear twice in their conversation list — one showing the last message preview and the other showing "No Messages". Backend investigation confirmed no duplicate conversations exist in the database, making this purely a frontend issue. The root cause is a race condition in WebSocket event delivery. When a conversation is created via the API with auto-assignment, the backend enqueues multiple ActionCable broadcast jobs (`conversation.created`, `assignee.changed`, `team.changed`) within milliseconds of each other. In production with multi-threaded Sidekiq workers, these events can arrive at the frontend out of order. If `assignee.changed` arrives before `conversation.created`, the `UPDATE_CONVERSATION` mutation pushes the conversation into the store (since it doesn't exist yet), and then `ADD_CONVERSATION` blindly pushes it again — resulting in a duplicate entry. The fix adds a uniqueness check in the `ADD_CONVERSATION` mutation to skip the push if a conversation with the same ID already exists in the store, matching the dedup pattern already used by `SET_ALL_CONVERSATION`. --- .../dashboard/store/modules/conversations/index.js | 5 ++++- .../modules/specs/conversations/mutations.spec.js | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js index 84be116fe..d7137694e 100644 --- a/app/javascript/dashboard/store/modules/conversations/index.js +++ b/app/javascript/dashboard/store/modules/conversations/index.js @@ -228,7 +228,10 @@ export const mutations = { }, [types.ADD_CONVERSATION](_state, conversation) { - _state.allConversations.push(conversation); + const exists = _state.allConversations.some(c => c.id === conversation.id); + if (!exists) { + _state.allConversations.push(conversation); + } }, [types.DELETE_CONVERSATION](_state, conversationId) { diff --git a/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js index 01abf05f7..bd048dbba 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js @@ -975,6 +975,16 @@ describe('#mutations', () => { mutations[types.ADD_CONVERSATION](state, conversation); expect(state.allConversations).toEqual([conversation]); }); + + it('should not add a duplicate conversation', () => { + const conversation = { id: 1, messages: [] }; + const state = { + allConversations: [conversation], + }; + + mutations[types.ADD_CONVERSATION](state, { id: 1, messages: [] }); + expect(state.allConversations).toHaveLength(1); + }); }); describe('#DELETE_CONVERSATION', () => {