mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
fix: prevent duplicate message rendering in collaborative sessions
🔧 MESSAGE DEDUPLICATION: Eliminate double rendering when second user sends messages ## Root Cause Analysis: Messages from other users were being processed by multiple event handlers: 1. Regular 'message' event handler 2. Goose message detection (heuristic) 3. Session message handler This caused the same message to appear multiple times in the chat. ## Solution Implementation: ### Enhanced Message Classification: ✅ Added message type flags (isGooseMessage, isSessionMessage) ✅ Use else-if chain to ensure only one handler processes each message ✅ Prevent multiple event emissions for the same message ### Smart Event Routing: ✅ Regular messages: Only emit 'message' if not Goose/session message ✅ Goose messages: Emit 'gooseMessage' + special 'gooseSessionSync' for collaboration ✅ Session messages: Emit 'sessionMessage' for legacy session handling ✅ Structured logging to track which handler processes each message ### Enhanced Matrix Context: ✅ Updated onMessage to support both default and named event listeners ✅ Overloaded function signature for backward compatibility ✅ Support for 'gooseSessionSync' event in useSessionSharing ### Session Sharing Integration: ✅ Added listener for 'gooseSessionSync' events ✅ Proper cleanup for all event listeners ✅ Maintains existing functionality while preventing duplicates ## Technical Flow: 1. Message received → Classify as regular/Goose/session 2. Emit appropriate event(s) → Only one primary handler per message 3. UI renders once → No duplicate message components 4. Collaboration sync → Works via dedicated 'gooseSessionSync' event ## Benefits: 🎯 **No More Duplicates**: Each message renders exactly once 🔄 **Preserved Functionality**: All existing features still work 🦆 **Goose Communication**: Still detects and handles Goose messages 📝 **Session Sync**: Collaborative sessions work without duplicates 🧹 **Clean Logging**: Clear indication of message processing path Now when a second user sends a message in a collaborative session, it will appear exactly once with the correct sender avatar! 🦆✨
This commit is contained in:
@@ -38,7 +38,7 @@ interface MatrixContextType {
|
||||
getOrCreateDirectMessageRoom: (userId: string) => Promise<string>;
|
||||
|
||||
// Events
|
||||
onMessage: (callback: (data: any) => void) => () => void;
|
||||
onMessage: ((callback: (data: any) => void) => () => void) & ((eventName: string, callback: (data: any) => void) => () => void);
|
||||
onAIMessage: (callback: (message: GooseAIMessage) => void) => () => void;
|
||||
onGooseMessage: (callback: (message: GooseChatMessage) => void) => () => void;
|
||||
onSessionMessage: (callback: (data: any) => void) => () => void;
|
||||
@@ -191,9 +191,18 @@ export const MatrixProvider: React.FC<MatrixProviderProps> = ({ children, matrix
|
||||
await matrixService.setDisplayName(displayName);
|
||||
};
|
||||
|
||||
const onMessage = (callback: (data: any) => void) => {
|
||||
matrixService.on('message', callback);
|
||||
return () => matrixService.off('message', callback);
|
||||
const onMessage = (eventNameOrCallback: string | ((data: any) => void), callback?: (data: any) => void) => {
|
||||
if (typeof eventNameOrCallback === 'string' && callback) {
|
||||
// Support for specific event names like 'gooseSessionSync'
|
||||
matrixService.on(eventNameOrCallback, callback);
|
||||
return () => matrixService.off(eventNameOrCallback, callback);
|
||||
} else if (typeof eventNameOrCallback === 'function') {
|
||||
// Default behavior for 'message' event
|
||||
matrixService.on('message', eventNameOrCallback);
|
||||
return () => matrixService.off('message', eventNameOrCallback);
|
||||
} else {
|
||||
throw new Error('Invalid arguments for onMessage');
|
||||
}
|
||||
};
|
||||
|
||||
const onAIMessage = (callback: (message: GooseAIMessage) => void) => {
|
||||
|
||||
@@ -208,10 +208,12 @@ export const useSessionSharing = ({
|
||||
|
||||
const sessionCleanup = onSessionMessage(handleSessionMessage);
|
||||
const messageCleanup = onMessage(handleRegularMessage);
|
||||
const gooseSessionCleanup = onMessage('gooseSessionSync', handleRegularMessage);
|
||||
|
||||
return () => {
|
||||
sessionCleanup();
|
||||
messageCleanup();
|
||||
gooseSessionCleanup();
|
||||
};
|
||||
}, [isConnected, sessionId, state.roomId, currentUser?.userId, onSessionMessage, onMessage, onMessageSync, onParticipantJoin]);
|
||||
|
||||
|
||||
@@ -495,8 +495,12 @@ export class MatrixService extends EventEmitter {
|
||||
senderInfo,
|
||||
};
|
||||
|
||||
let isGooseMessage = false;
|
||||
let isSessionMessage = false;
|
||||
|
||||
// Check if this is a structured Goose message (new format)
|
||||
if (content['goose.message.type']) {
|
||||
isGooseMessage = true;
|
||||
const gooseChatMessage: GooseChatMessage = {
|
||||
type: content['goose.message.type'] as any,
|
||||
messageId: content['goose.message.id'] || this.generateMessageId(),
|
||||
@@ -517,14 +521,13 @@ export class MatrixService extends EventEmitter {
|
||||
},
|
||||
};
|
||||
|
||||
console.log('🦆 Received Goose message:', gooseChatMessage.type, 'from:', sender, isFromSelf ? '(self)' : '(other)');
|
||||
console.log('🦆 Received structured Goose message:', gooseChatMessage.type, 'from:', sender, isFromSelf ? '(self)' : '(other)');
|
||||
this.emit('gooseMessage', gooseChatMessage);
|
||||
|
||||
// Don't return early - let it also be processed as regular message
|
||||
}
|
||||
|
||||
// Check if this is a legacy Goose AI message (using custom properties)
|
||||
if (content['goose.type']) {
|
||||
else if (content['goose.type']) {
|
||||
isGooseMessage = true;
|
||||
const aiMessage: GooseAIMessage = {
|
||||
type: `ai.${content['goose.type']}` as any,
|
||||
sessionId: content['goose.session_id'] || room.roomId,
|
||||
@@ -535,14 +538,13 @@ export class MatrixService extends EventEmitter {
|
||||
metadata: { ...content, isFromSelf },
|
||||
};
|
||||
|
||||
console.log('🦆 Received legacy Goose AI message:', aiMessage.type, 'from:', sender, isFromSelf ? '(self)' : '(other)');
|
||||
this.emit('aiMessage', aiMessage);
|
||||
|
||||
// Don't return early for legacy messages either
|
||||
}
|
||||
|
||||
// Check if sender appears to be a Goose instance (heuristic detection) - but not ourselves
|
||||
const senderUser = this.client?.getUser(sender);
|
||||
if (!isFromSelf && this.isGooseInstance(sender, senderUser?.displayName)) {
|
||||
else if (!isFromSelf && this.isGooseInstance(sender, senderUser?.displayName)) {
|
||||
isGooseMessage = true;
|
||||
// Treat as potential Goose message even without explicit markers
|
||||
const gooseChatMessage: GooseChatMessage = {
|
||||
type: 'goose.chat',
|
||||
@@ -558,20 +560,35 @@ export class MatrixService extends EventEmitter {
|
||||
this.emit('gooseMessage', gooseChatMessage);
|
||||
}
|
||||
|
||||
// Always emit regular message event (but skip our own messages to avoid echo in regular chat)
|
||||
if (!isFromSelf) {
|
||||
this.emit('message', messageData);
|
||||
}
|
||||
|
||||
// Also check if this is a session-related message and emit as session message
|
||||
// Check if this is a session-related message
|
||||
if (content.body && !isFromSelf) {
|
||||
// Check for Goose session messages (from useSessionSharing)
|
||||
if (content.body.includes('goose-session-message:') ||
|
||||
content.body.includes('goose-session-invite:') ||
|
||||
content.body.includes('goose-session-joined:')) {
|
||||
isSessionMessage = true;
|
||||
console.log('📝 Received session message from:', sender);
|
||||
this.emit('sessionMessage', messageData);
|
||||
}
|
||||
}
|
||||
|
||||
// Only emit regular message event if it's not already handled as a Goose message or session message
|
||||
// This prevents duplicate rendering
|
||||
if (!isFromSelf && !isGooseMessage && !isSessionMessage) {
|
||||
console.log('💬 Received regular message from:', sender);
|
||||
this.emit('message', messageData);
|
||||
}
|
||||
|
||||
// However, always emit for session messages in collaborative sessions
|
||||
// (even if they're from Goose instances) to ensure proper synchronization
|
||||
if (isSessionMessage) {
|
||||
// Already emitted above
|
||||
} else if (!isFromSelf && isGooseMessage) {
|
||||
// For Goose messages that might also need to be treated as regular messages in collaborative sessions
|
||||
// We'll emit a special event that useSessionSharing can listen to
|
||||
console.log('🔄 Emitting Goose message as potential session sync from:', sender);
|
||||
this.emit('gooseSessionSync', messageData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user