[eric] fix duplicate text — dedup WebSocket stream/message events

Root cause: two WebSocket connections open to the same session
(dashboard card + expanded chat). Every event delivered twice.

Fix: track seen stream IDs and message IDs in WebSocketManager.
- stream_start: skip if message_id already seen
- stream_delta: only accept for streams WE initiated
- stream_end: dedup + clean up tracking
- agent:message: skip if message id already processed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ciregenz
2026-03-22 00:36:10 -07:00
co-authored by Claude Opus 4.6
parent 82212920ae
commit ed66c80f7d
@@ -38,6 +38,8 @@ class WebSocketManager {
private listeners: Map<string, Set<(data: any) => void>> = new Map();
private deltaBuffer: Map<string, { sessionId: string; messageId: string; accumulated: string }> = new Map();
private flushScheduled = false;
private seenStreamIds: Set<string> = new Set();
private seenMessageIds: Set<string> = new Set();
constructor(url: string, options?: WSManagerOptions) {
this.url = url;
@@ -133,6 +135,9 @@ class WebSocketManager {
case 'agent:message':
if (session_id && data.message) {
const msgId = data.message.id;
if (msgId && this.seenMessageIds.has(msgId)) break; // Dedup
if (msgId) this.seenMessageIds.add(msgId);
if (this.deltaBuffer.size > 0) this.flushDeltas();
store.dispatch(addMessage({ sessionId: session_id, message: data.message }));
}
@@ -140,6 +145,8 @@ class WebSocketManager {
case 'agent:stream_start':
if (session_id && data.message_id) {
if (this.seenStreamIds.has(data.message_id)) break; // Dedup
this.seenStreamIds.add(data.message_id);
store.dispatch(streamStart({
sessionId: session_id,
messageId: data.message_id,
@@ -151,12 +158,15 @@ class WebSocketManager {
case 'agent:stream_delta':
if (session_id && data.message_id) {
if (!this.seenStreamIds.has(data.message_id)) break; // Only accept deltas for streams WE started
this.bufferDelta(session_id, data.message_id, data.delta);
}
break;
case 'agent:stream_end':
if (session_id && data.message_id) {
if (!this.seenStreamIds.has(data.message_id)) break; // Dedup
this.seenStreamIds.delete(data.message_id); // Clean up
if (this.deltaBuffer.size > 0) this.flushDeltas();
store.dispatch(streamEnd({
sessionId: session_id,