Skip to content

fix: dedupe realtime messages before render#616

Open
CoderLuii wants to merge 2 commits intositeboon:mainfrom
CoderLuii:fix/realtime-message-dedupe
Open

fix: dedupe realtime messages before render#616
CoderLuii wants to merge 2 commits intositeboon:mainfrom
CoderLuii:fix/realtime-message-dedupe

Conversation

@CoderLuii
Copy link
Copy Markdown

@CoderLuii CoderLuii commented Apr 4, 2026

Summary

  • upsert equivalent realtime messages instead of blindly appending them
  • dedupe realtime messages against persisted server messages when computing the merged session view

Testing

  • validated in HolyClaude integration flow while reproducing duplicate message and duplicate thinking-state rendering

Summary by CodeRabbit

  • Bug Fixes
    • Improved real-time message deduplication with a more robust equivalence comparator that accounts for multiple identifier types and fallback mechanisms, ensuring more reliable duplicate detection and message update handling across batches.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

Updates message deduplication in the session store from simple ID equality to a sophisticated equivalence comparison that considers multiple identifier fields (id, rowid, sequence, requestId, toolId) with special handling for user messages and session creation events.

Changes

Cohort / File(s) Summary
Message Deduplication Logic
src/stores/useSessionStore.ts
Added isEquivalentNormalizedMessage() comparator supporting fallback matching via id, rowid+kind, sequence+kind, requestId+kind, and toolId+kind; implemented upsertRealtimeMessage() to replace existing equivalent messages or append new ones; refactored appendRealtime and appendRealtimeBatch to use upsert instead of simple append, enabling deduped batch processing.

Suggested reviewers

  • viper151
  • blackmammoth

Poem

🐰 Hops with glee through deduplicated streams,
No more duplicate dreams in our message beams,
With fallbacks so clever and comparisons deep,
Your session store now keeps messages neat and unique!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: implementing deduplication of realtime messages before rendering, which matches the core objective of preventing duplicate message and thinking-state rendering.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/stores/useSessionStore.ts (1)

342-346: ⚠️ Potential issue | 🟡 Minor

Skip server-equivalent messages before storing them.

These paths still retain messages that computeMerged() will immediately hide because an equivalent server message already exists. Those hidden entries still count toward MAX_REALTIME_MESSAGES, so a burst of duplicates can evict genuinely pending realtime items from the buffer. Since this store already treats serverMessages as the source of truth, short-circuiting here would keep the capped realtime buffer reserved for not-yet-persisted events.

Also applies to: 357-363

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/useSessionStore.ts` around lines 342 - 346, Before adding to
slot.realtimeMessages, check whether the incoming msg is effectively already
present in the authoritative server view and skip storing it—i.e., call the same
equivalence check used by computeMerged() or compare against slot.serverMessages
and, if an equivalent server message exists, do not call upsertRealtimeMessage.
Update the two places that currently call upsertRealtimeMessage (the block using
upsertRealtimeMessage(slot.realtimeMessages, msg) and the similar block later at
357-363) to short-circuit when a server-equivalent message is found so the
realtime buffer only counts truly pending, not-already-persisted events; keep
MAX_REALTIME_MESSAGES trimming logic unchanged for actual inserts. Ensure you
reference computeMerged, serverMessages, realtimeMessages, upsertRealtimeMessage
and MAX_REALTIME_MESSAGES when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/stores/useSessionStore.ts`:
- Around line 139-145: The current equality logic in useSessionStore (the
comparisons using a.requestId, b.requestId, a.toolId, b.toolId and a.kind)
considers requestId before toolId which collapses multiple same-kind messages
from the same request; change the comparator to disambiguate by tool identity
first (check toolId equality with a.kind) or require both toolId and requestId
to match before returning true, and if applicable include the same identity
fields used in src/components/chat/utils/messageKeys.ts (toolId, rowid,
sequence) when determining message identity so that repeated
tool_use/tool_result events are not merged incorrectly.

---

Outside diff comments:
In `@src/stores/useSessionStore.ts`:
- Around line 342-346: Before adding to slot.realtimeMessages, check whether the
incoming msg is effectively already present in the authoritative server view and
skip storing it—i.e., call the same equivalence check used by computeMerged() or
compare against slot.serverMessages and, if an equivalent server message exists,
do not call upsertRealtimeMessage. Update the two places that currently call
upsertRealtimeMessage (the block using
upsertRealtimeMessage(slot.realtimeMessages, msg) and the similar block later at
357-363) to short-circuit when a server-equivalent message is found so the
realtime buffer only counts truly pending, not-already-persisted events; keep
MAX_REALTIME_MESSAGES trimming logic unchanged for actual inserts. Ensure you
reference computeMerged, serverMessages, realtimeMessages, upsertRealtimeMessage
and MAX_REALTIME_MESSAGES when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 31ebe943-a048-40c7-b32f-98bb83831048

📥 Commits

Reviewing files that changed from the base of the PR and between 388134c and ada3881.

📒 Files selected for processing (1)
  • src/stores/useSessionStore.ts

Comment on lines +139 to +145
if (a.requestId && b.requestId && a.requestId === b.requestId && a.kind === b.kind) {
return true;
}

if (a.toolId && b.toolId && a.toolId === b.toolId && a.kind === b.kind) {
return true;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

requestId is too broad to outrank toolId here.

Line 139 matches on requestId before Line 143 gets a chance to disambiguate by toolId. That can collapse multiple same-kind messages from one request—especially repeated tool_use / tool_result events—into a single entry. src/components/chat/utils/messageKeys.ts:10-24 points the same way: it treats toolId, rowid, and sequence as intrinsic identity, but never uses requestId.

🔧 Narrow fix
-  if (a.requestId && b.requestId && a.requestId === b.requestId && a.kind === b.kind) {
-    return true;
-  }
-
   if (a.toolId && b.toolId && a.toolId === b.toolId && a.kind === b.kind) {
     return true;
   }
+
+  if (
+    a.requestId &&
+    b.requestId &&
+    a.requestId === b.requestId &&
+    a.kind === b.kind &&
+    (a.toolId === undefined || b.toolId === undefined || a.toolId === b.toolId)
+  ) {
+    return true;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/useSessionStore.ts` around lines 139 - 145, The current equality
logic in useSessionStore (the comparisons using a.requestId, b.requestId,
a.toolId, b.toolId and a.kind) considers requestId before toolId which collapses
multiple same-kind messages from the same request; change the comparator to
disambiguate by tool identity first (check toolId equality with a.kind) or
require both toolId and requestId to match before returning true, and if
applicable include the same identity fields used in
src/components/chat/utils/messageKeys.ts (toolId, rowid, sequence) when
determining message identity so that repeated tool_use/tool_result events are
not merged incorrectly.

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