From c1ed8470ae8c17937405b7dd57754e67304559da Mon Sep 17 00:00:00 2001 From: Nuwan Date: Sat, 7 Feb 2026 01:38:50 +0530 Subject: [PATCH] fix(16): improve attachment deduplication by attachmentId The optimistic message uses 'attachment-{notation.id}' as ID while REST API and WebSocket use the chat message ID. This caused duplicates when fetchChatHistory ran after an optimistic upload. Now deduplication checks both message ID and attachmentId for attachment messages, preventing duplicates regardless of ID format. Co-Authored-By: Claude Opus 4.5 --- jam-ui/src/store/features/sessionChatSlice.js | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/jam-ui/src/store/features/sessionChatSlice.js b/jam-ui/src/store/features/sessionChatSlice.js index 414d6edff..2738aa903 100644 --- a/jam-ui/src/store/features/sessionChatSlice.js +++ b/jam-ui/src/store/features/sessionChatSlice.js @@ -185,9 +185,18 @@ const sessionChatSlice = createSlice({ state.messagesByChannel[channel] = []; } - // Deduplicate by msg_id - const exists = state.messagesByChannel[channel].some(m => m.id === message.id); - if (exists) return; + // Deduplicate by msg_id and attachmentId (for attachment messages) + const existsById = state.messagesByChannel[channel].some(m => m.id === message.id); + if (existsById) return; + + // For attachment messages, also check by attachmentId to avoid duplicates + // from optimistic updates that use different ID format + if (message.attachmentId) { + const existsByAttachmentId = state.messagesByChannel[channel].some( + m => m.attachmentId && m.attachmentId === message.attachmentId + ); + if (existsByAttachmentId) return; + } // Add message state.messagesByChannel[channel].push(message); @@ -335,9 +344,23 @@ const sessionChatSlice = createSlice({ attachmentSize: null // Not available in REST API response (only in WebSocket) })); - // Deduplicate messages + // Deduplicate messages by ID and by attachmentId (for attachment messages) + // This handles the case where optimistic attachment message uses 'attachment-{id}' format + // but the REST API returns the chat message ID const existingIds = new Set(state.messagesByChannel[channel].map(m => m.id)); - const newMessages = transformedMessages.filter(m => !existingIds.has(m.id)); + const existingAttachmentIds = new Set( + state.messagesByChannel[channel] + .filter(m => m.attachmentId) + .map(m => m.attachmentId) + ); + const newMessages = transformedMessages.filter(m => { + // Check by message ID first + if (existingIds.has(m.id)) return false; + // For attachment messages, also check by attachmentId to avoid duplicates + // from optimistic updates that use different ID format + if (m.attachmentId && existingAttachmentIds.has(m.attachmentId)) return false; + return true; + }); // Prepend new messages (oldest first for pagination) state.messagesByChannel[channel] = [...newMessages, ...state.messagesByChannel[channel]];