Skip to content

fix(playground): key chat messages by sender and timestamp - #2275

Open
leepokai wants to merge 1 commit into
TEN-framework:mainfrom
leepokai:fix/chat-duplicate-key
Open

fix(playground): key chat messages by sender and timestamp#2275
leepokai wants to merge 1 commit into
TEN-framework:mainfrom
leepokai:fix/chat-duplicate-key

Conversation

@leepokai

@leepokai leepokai commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Problem

The chat list throws a React key collision during normal conversation:

Encountered two children with the same key, `1785829362590`.
Keys should be unique so that components maintain their identity across updates.
Non-unique keys may cause children to be duplicated and/or omitted.

The key is a millisecond timestamp, so this is not purely cosmetic — React warns that colliding keys can drop or duplicate rendered children, meaning chat lines can visibly disappear or repeat.

Root cause

MessageList keys each row by item.time alone:

return <MessageItem data={item} key={item.time} />;

item.time is text_ts, stamped with int(time.time() * 1000) when the transcript is sent, and item.userId is the sender's stream_id. The addChatItem reducer scopes both its discard and its replace branches to a single userId:

const LastFinalIndex = state.chatItems.findLastIndex(
  (el) => el.userId === userId && el.isFinal
);

So two transcripts from different senders that share a millisecond are both pushed, and the list renders two children with the same key.

Two transcripts from the same sender can never collide — the reducer either discards the later one (time <= LastFinalItem.time) or replaces the pending non-final item.

Speech-to-speech graphs make the cross-sender collision routine rather than rare: the final input transcription is emitted as the first output transcript delta arrives, and the two are sent with different stream_ids.

Fix

Key by sender and timestamp together, which is unique by the argument above.

Verification

Reproduced by replaying the reducer with a user transcript (stream_id from session metadata) and an agent transcript (stream_id 100) sharing one millisecond — two items, one key. With the composite key both rows are unique, and the same-sender case still collapses to a single item, so no row is lost.

Confirmed against a running voice_assistant_realtime graph: the warning fired twice during ordinary conversation before the change and does not appear after it.

`MessageList` keyed each row by `item.time` alone, a millisecond timestamp
stamped when the transcript is sent. `addChatItem` scopes its dedupe to a
single `userId`, so a user transcript and an agent transcript that share a
millisecond are both pushed and React sees two children with one key.

Speech-to-speech graphs make that routine rather than rare: the final input
transcription lands as the first output transcript delta arrives, and the two
are sent with different stream_ids, so neither the dedupe nor the discard
branch collapses them.

Keying by sender and timestamp together is enough. Two items from the same
sender can never collide, because the reducer already discards or replaces
them before they reach the list.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant