fix: prevent chat messages from duplicating or disappearing (#507)

- Tighten message deduplication and turn-structure checks in chat rendering
- Avoid stale cached turn grouping data during streaming and incremental updates
- Keep message order stable so headers and grouped messages render consistently
This commit is contained in:
Bohdan Triapitsyn
2026-02-25 15:20:38 +02:00
committed by GitHub
parent 10851bd7ac
commit 5473381720
2 changed files with 27 additions and 11 deletions
+20 -11
View File
@@ -57,6 +57,10 @@ const hasSameTurnStructure = (prev: ChatMessageEntry[], next: ChatMessageEntry[]
const prevMessage = prev[index]; const prevMessage = prev[index];
const nextMessage = next[index]; const nextMessage = next[index];
if (prevMessage !== nextMessage) {
return false;
}
if (prevMessage.info.id !== nextMessage.info.id) { if (prevMessage.info.id !== nextMessage.info.id) {
return false; return false;
} }
@@ -466,19 +470,24 @@ const MessageList: React.FC<MessageListProps> = ({
}, [permissions, questions, onMessageContentChange]); }, [permissions, questions, onMessageContentChange]);
const baseDisplayMessages = React.useMemo(() => { const baseDisplayMessages = React.useMemo(() => {
const seenIds = new Set<string>(); const seenIdsFromTail = new Set<string>();
const nextNormalizedCache = new Map<string, { source: ChatMessageEntry; normalized: ChatMessageEntry }>(); const nextNormalizedCache = new Map<string, { source: ChatMessageEntry; normalized: ChatMessageEntry }>();
const normalizedMessages = messages
.filter((message) => { const dedupedMessages: ChatMessageEntry[] = [];
const messageId = message.info?.id; for (let index = messages.length - 1; index >= 0; index -= 1) {
if (typeof messageId === 'string') { const message = messages[index];
if (seenIds.has(messageId)) { const messageId = message.info?.id;
return false; if (typeof messageId === 'string') {
} if (seenIdsFromTail.has(messageId)) {
seenIds.add(messageId); continue;
} }
return true; seenIdsFromTail.add(messageId);
}) }
dedupedMessages.push(message);
}
dedupedMessages.reverse();
const normalizedMessages = dedupedMessages
.map((message, index) => { .map((message, index) => {
const messageId = typeof message.info?.id === 'string' && message.info.id.length > 0 const messageId = typeof message.info?.id === 'string' && message.info.id.length > 0
? message.info.id ? message.info.id
@@ -519,6 +519,9 @@ const hasSameTurnStructure = (prev: ChatMessageEntry[], next: ChatMessageEntry[]
} }
for (let index = 0; index < prev.length; index += 1) { for (let index = 0; index < prev.length; index += 1) {
if (prev[index] !== next[index]) {
return false;
}
if (prev[index]?.info?.id !== next[index]?.info?.id) { if (prev[index]?.info?.id !== next[index]?.info?.id) {
return false; return false;
} }
@@ -543,6 +546,9 @@ const isAppendOnlyChange = (prev: ChatMessageEntry[], next: ChatMessageEntry[]):
} }
for (let index = 0; index < prev.length; index += 1) { for (let index = 0; index < prev.length; index += 1) {
if (prev[index] !== next[index]) {
return false;
}
if (prev[index]?.info?.id !== next[index]?.info?.id) { if (prev[index]?.info?.id !== next[index]?.info?.id) {
return false; return false;
} }
@@ -658,6 +664,7 @@ export const TurnGroupingProvider: React.FC<TurnGroupingProviderProps> = ({ mess
const cached = staticCacheRef.current; const cached = staticCacheRef.current;
if ( if (
cached && cached &&
hasSameTurnStructure(cached.messages, messages) &&
cached.structureKey === structureKey && cached.structureKey === structureKey &&
cached.defaultActivityExpanded === defaultActivityExpanded && cached.defaultActivityExpanded === defaultActivityExpanded &&
cached.showTextJustificationActivity === showTextJustificationActivity cached.showTextJustificationActivity === showTextJustificationActivity