mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Fix Matrix message streaming - only send complete messages once
- Added sentToMatrixRef to track sent messages and prevent duplicates - Only send messages when chatState is Idle (streaming complete) - Format messages as goose-session-message: for proper parsing by receivers - Prevents 429 Too Many Requests errors from sending during streaming
This commit is contained in:
@@ -107,6 +107,7 @@ function BaseChatContent({
|
||||
sessionLoadError,
|
||||
setRecipeUserParams,
|
||||
tokenState,
|
||||
gooseEnabled,
|
||||
} = useChatStream({
|
||||
sessionId,
|
||||
onStreamFinish,
|
||||
@@ -544,6 +545,7 @@ function BaseChatContent({
|
||||
toolCount={toolCount || 0}
|
||||
autoSubmit={false}
|
||||
append={append}
|
||||
gooseEnabled={gooseEnabled}
|
||||
{...customChatInputProps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -103,6 +103,7 @@ interface ChatInputProps {
|
||||
autoSubmit: boolean;
|
||||
append?: (message: Message) => void;
|
||||
isExtensionsLoading?: boolean;
|
||||
gooseEnabled?: boolean;
|
||||
}
|
||||
|
||||
export default function ChatInput({
|
||||
@@ -130,6 +131,7 @@ export default function ChatInput({
|
||||
autoSubmit = false,
|
||||
append,
|
||||
isExtensionsLoading = false,
|
||||
gooseEnabled = true,
|
||||
}: ChatInputProps) {
|
||||
// Track the available width for responsive layout
|
||||
const [availableWidth, setAvailableWidth] = useState(window.innerWidth);
|
||||
@@ -478,14 +480,17 @@ export default function ChatInput({
|
||||
},
|
||||
});
|
||||
|
||||
// Listen for AI responses to sync to Matrix
|
||||
// Track which messages have been sent to Matrix to prevent duplicates
|
||||
const sentToMatrixRef = useRef<Set<string>>(new Set());
|
||||
|
||||
// Listen for AI responses to sync to Matrix or collaborative sessions
|
||||
// FIXED: Robust null checking to prevent "Cannot read properties of undefined (reading 'length')" error
|
||||
// Updated: Fixed all commandHistory.length accesses with safeCommandHistory
|
||||
// Final fix: All .length accesses now properly null-checked
|
||||
// CRITICAL FIX: Only send to Matrix when streaming is complete (chatState is Idle)
|
||||
// CRITICAL FIX 2: Track sent messages to prevent duplicate sends
|
||||
useEffect(() => {
|
||||
// CRITICAL: Only sync AI responses for non-Matrix rooms
|
||||
// Matrix rooms handle AI responses directly through Matrix, not through sync
|
||||
if (!sessionSharing.isSessionActive || isMatrixRoom || !messages || !Array.isArray(messages) || messages.length === 0) return;
|
||||
if (!messages || !Array.isArray(messages) || messages.length === 0) return;
|
||||
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
|
||||
@@ -498,7 +503,19 @@ export default function ChatInput({
|
||||
!lastMessage.sender && // Messages from Matrix have sender info, local AI responses don't
|
||||
!lastMessage.metadata?.isFromMatrix && // Additional check for Matrix-originated messages
|
||||
!lastMessage.metadata?.isFromCollaborator) { // Additional check for collaborator messages
|
||||
console.log('🤖 Syncing AI response to collaborative session (non-Matrix):', lastMessage);
|
||||
|
||||
// CRITICAL: Only send to Matrix when streaming is complete (chatState is Idle)
|
||||
// This prevents sending partial messages during streaming
|
||||
if (chatState !== ChatState.Idle) {
|
||||
console.log('🚫 Skipping Matrix sync - streaming in progress (chatState:', chatState, ')');
|
||||
return;
|
||||
}
|
||||
|
||||
// CRITICAL: Check if we've already sent this message to Matrix
|
||||
if (sentToMatrixRef.current.has(lastMessage.id)) {
|
||||
console.log('🚫 Skipping Matrix sync - message already sent:', lastMessage.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract text content from the message - with robust null checking
|
||||
let textContent = '';
|
||||
@@ -511,7 +528,35 @@ export default function ChatInput({
|
||||
textContent = lastMessage.content;
|
||||
}
|
||||
|
||||
if (textContent.trim()) {
|
||||
if (!textContent.trim()) return;
|
||||
|
||||
// Handle Matrix rooms: send AI response directly to Matrix with goose-session-message format
|
||||
if (isMatrixRoom && actualMatrixRoomId && sendMessage) {
|
||||
console.log('🤖 Sending COMPLETE AI response to Matrix room:', actualMatrixRoomId, '(chatState:', chatState, ', messageId:', lastMessage.id, ')');
|
||||
|
||||
// Mark as sent BEFORE sending to prevent race conditions
|
||||
sentToMatrixRef.current.add(lastMessage.id);
|
||||
|
||||
// Format as goose-session-message so it can be properly parsed by other clients
|
||||
const sessionMessage = {
|
||||
sessionId: sessionId || actualMatrixRoomId,
|
||||
role: 'assistant',
|
||||
content: textContent,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
const formattedMessage = `goose-session-message:${JSON.stringify(sessionMessage)}`;
|
||||
|
||||
sendMessage(actualMatrixRoomId, formattedMessage).then(() => {
|
||||
console.log('✅ Successfully sent COMPLETE AI response to Matrix room (messageId:', lastMessage.id, ')');
|
||||
}).catch((error) => {
|
||||
console.error('❌ Failed to send AI response to Matrix room:', error);
|
||||
// Remove from sent set if it failed so we can retry
|
||||
sentToMatrixRef.current.delete(lastMessage.id);
|
||||
});
|
||||
}
|
||||
// Handle non-Matrix collaborative sessions: sync through sessionSharing
|
||||
else if (sessionSharing.isSessionActive && !isMatrixRoom) {
|
||||
console.log('🤖 Syncing COMPLETE AI response to collaborative session (non-Matrix):', lastMessage);
|
||||
sessionSharing.syncMessage({
|
||||
id: lastMessage.id || `ai-${Date.now()}`,
|
||||
role: 'assistant',
|
||||
@@ -520,7 +565,7 @@ export default function ChatInput({
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [messages, sessionSharing, isMatrixRoom]);
|
||||
}, [messages, sessionSharing, isMatrixRoom, actualMatrixRoomId, sendMessage, chatState]);
|
||||
|
||||
|
||||
|
||||
@@ -1871,6 +1916,18 @@ export default function ChatInput({
|
||||
{/* This space is reserved for mention popovers to render above the chat input */}
|
||||
</div>
|
||||
|
||||
{/* Goose Off Indicator */}
|
||||
{!gooseEnabled && (
|
||||
<div className="max-w-4xl mx-auto w-full mb-2">
|
||||
<div className="bg-yellow-500/10 border border-yellow-500/30 rounded-lg px-4 py-2 flex items-center gap-2">
|
||||
<div className="flex-shrink-0 w-2 h-2 bg-yellow-500 rounded-full animate-pulse" />
|
||||
<span className="text-yellow-600 dark:text-yellow-400 text-sm font-medium">
|
||||
Goose is OFF - Type <code className="px-1.5 py-0.5 bg-yellow-500/20 rounded text-xs">@goose</code> to reactivate
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Chat input container with max width - floating card */}
|
||||
<div className="max-w-4xl mx-auto w-full shadow-2xl drop-shadow-2xl">
|
||||
<div className="bg-background-default rounded-2xl pt-2 px-2">
|
||||
|
||||
@@ -915,7 +915,7 @@ export const TabProvider: React.FC<TabProviderProps> = ({ children }) => {
|
||||
messages: [],
|
||||
messageHistoryIndex: 0,
|
||||
recipeConfig: null,
|
||||
aiEnabled: false,
|
||||
aiEnabled: true, // Enable AI for Matrix chats so Goose can respond
|
||||
},
|
||||
loadingChat: true // Show loading state
|
||||
};
|
||||
|
||||
@@ -31,13 +31,7 @@ function checkForGooseCommands(message: string, currentGooseEnabled: boolean): {
|
||||
} {
|
||||
const trimmedMessage = message.trim().toLowerCase();
|
||||
|
||||
// Check for goose ON command
|
||||
if (trimmedMessage === '@goose' || trimmedMessage === '@goose on') {
|
||||
console.log('🦆 Goose turned ON');
|
||||
return { skipAI: true, newGooseEnabled: true, isGooseCommand: true };
|
||||
}
|
||||
|
||||
// Check for goose OFF commands
|
||||
// Check for goose OFF commands (must be exact matches)
|
||||
const gooseOffCommands = ['@goose off', '@goose stop', '@goose quiet', '@goose pause'];
|
||||
for (const command of gooseOffCommands) {
|
||||
if (trimmedMessage === command) {
|
||||
@@ -46,7 +40,17 @@ function checkForGooseCommands(message: string, currentGooseEnabled: boolean): {
|
||||
}
|
||||
}
|
||||
|
||||
// If goose is disabled, skip all AI responses except for goose commands
|
||||
// Check for @goose mention anywhere in the message (case-insensitive)
|
||||
const gooseMentionPattern = /@goose\b/i;
|
||||
const containsGooseMention = gooseMentionPattern.test(message);
|
||||
|
||||
// If goose is disabled and message contains @goose, turn it back on
|
||||
if (!currentGooseEnabled && containsGooseMention) {
|
||||
console.log('🦆 Goose turned ON - detected @goose mention in message');
|
||||
return { skipAI: false, newGooseEnabled: true, isGooseCommand: false };
|
||||
}
|
||||
|
||||
// If goose is disabled and no @goose mention, skip AI response
|
||||
if (!currentGooseEnabled) {
|
||||
console.log('🦆 Goose is OFF - skipping AI response');
|
||||
return { skipAI: true, newGooseEnabled: currentGooseEnabled, isGooseCommand: false };
|
||||
@@ -118,6 +122,7 @@ interface UseChatStreamReturn {
|
||||
stopStreaming: () => void;
|
||||
sessionLoadError?: string;
|
||||
tokenState: TokenState;
|
||||
gooseEnabled: boolean;
|
||||
}
|
||||
|
||||
function pushMessage(currentMessages: Message[], incomingMsg: Message): Message[] {
|
||||
@@ -777,5 +782,6 @@ export function useChatStream({
|
||||
stopStreaming,
|
||||
setRecipeUserParams,
|
||||
tokenState,
|
||||
gooseEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user