frontend(thread): avoid streaming for inactive agent runs and quiet expected not-running/404 cases\n\n- Thread page: only auto-start streaming on load when agentStatus === 'running'\n- useAgentStream: pre-check status; finalize quietly for not-running/not-found; suppress toasts for expected cases\n- Reduce noisy logs/toasts when opening historical threads

This commit is contained in:
nettanvirdev 2025-08-24 23:31:43 +06:00
parent dad49f8a50
commit 74b89f4020
2 changed files with 362 additions and 194 deletions

View File

@ -19,15 +19,31 @@ import { isLocalMode } from '@/lib/config';
import { ThreadContent } from '@/components/thread/content/ThreadContent';
import { ThreadSkeleton } from '@/components/thread/content/ThreadSkeleton';
import { useAddUserMessageMutation } from '@/hooks/react-query/threads/use-messages';
import { useStartAgentMutation, useStopAgentMutation } from '@/hooks/react-query/threads/use-agent-run';
import {
useStartAgentMutation,
useStopAgentMutation,
} from '@/hooks/react-query/threads/use-agent-run';
import { useSharedSubscription } from '@/contexts/SubscriptionContext';
import { SubscriptionStatus } from '@/components/thread/chat-input/_use-model-selection';
import { UnifiedMessage, ApiMessageType, ToolCallInput, Project } from '../_types';
import { useThreadData, useToolCalls, useBilling, useKeyboardShortcuts } from '../_hooks';
import {
UnifiedMessage,
ApiMessageType,
ToolCallInput,
Project,
} from '../_types';
import {
useThreadData,
useToolCalls,
useBilling,
useKeyboardShortcuts,
} from '../_hooks';
import { ThreadError, UpgradeDialog, ThreadLayout } from '../_components';
import { useThreadAgent, useAgents } from '@/hooks/react-query/agents/use-agents';
import {
useThreadAgent,
useAgents,
} from '@/hooks/react-query/agents/use-agents';
import { AgentRunLimitDialog } from '@/components/thread/agent-run-limit-dialog';
import { useAgentSelection } from '@/lib/stores/agent-selection-store';
import { useQueryClient } from '@tanstack/react-query';
@ -53,17 +69,20 @@ export default function ThreadPage({
const [isSending, setIsSending] = useState(false);
const [fileViewerOpen, setFileViewerOpen] = useState(false);
const [fileToView, setFileToView] = useState<string | null>(null);
const [filePathList, setFilePathList] = useState<string[] | undefined>(undefined);
const [filePathList, setFilePathList] = useState<string[] | undefined>(
undefined,
);
const [showUpgradeDialog, setShowUpgradeDialog] = useState(false);
const [debugMode, setDebugMode] = useState(false);
const [initialPanelOpenAttempted, setInitialPanelOpenAttempted] = useState(false);
const [initialPanelOpenAttempted, setInitialPanelOpenAttempted] =
useState(false);
// Use Zustand store for agent selection persistence
const {
selectedAgentId,
setSelectedAgent,
initializeFromAgents,
getCurrentAgent,
isSunaAgent
isSunaAgent,
} = useAgentSelection();
const { data: agentsResponse } = useAgents();
@ -77,7 +96,6 @@ export default function ThreadPage({
runningThreadIds: string[];
} | null>(null);
// Refs - simplified for flex-column-reverse
const latestMessageRef = useRef<HTMLDivElement>(null);
const initialLayoutAppliedRef = useRef(false);
@ -165,12 +183,13 @@ export default function ThreadPage({
}, [threadAgentData, agents, initializeFromAgents]);
const { data: subscriptionData } = useSharedSubscription();
const subscriptionStatus: SubscriptionStatus = (subscriptionData?.status === 'active' || subscriptionData?.status === 'trialing')
const subscriptionStatus: SubscriptionStatus =
subscriptionData?.status === 'active' ||
subscriptionData?.status === 'trialing'
? 'active'
: 'no_subscription';
const handleProjectRenamed = useCallback((newName: string) => {
}, []);
const handleProjectRenamed = useCallback((newName: string) => {}, []);
// scrollToBottom for flex-column-reverse layout
const scrollToBottom = useCallback(() => {
@ -179,7 +198,8 @@ export default function ThreadPage({
}
}, []);
const handleNewMessageFromStream = useCallback((message: UnifiedMessage) => {
const handleNewMessageFromStream = useCallback(
(message: UnifiedMessage) => {
if (!message.message_id) {
console.warn(
`[STREAM HANDLER] Received message is missing ID: Type=${message.type}`,
@ -197,15 +217,16 @@ export default function ThreadPage({
} else {
// If this is a user message, replace any optimistic user message with temp ID
if (message.type === 'user') {
const optimisticIndex = prev.findIndex(m =>
const optimisticIndex = prev.findIndex(
(m) =>
m.type === 'user' &&
m.message_id?.startsWith('temp-') &&
m.content === message.content
m.content === message.content,
);
if (optimisticIndex !== -1) {
// Replace the optimistic message with the real one
return prev.map((m, index) =>
index === optimisticIndex ? message : m
index === optimisticIndex ? message : m,
);
}
}
@ -216,9 +237,12 @@ export default function ThreadPage({
if (message.type === 'tool') {
setAutoOpenedPanel(false);
}
}, [setMessages, setAutoOpenedPanel]);
},
[setMessages, setAutoOpenedPanel],
);
const handleStreamStatusChange = useCallback((hookStatus: string) => {
const handleStreamStatusChange = useCallback(
(hookStatus: string) => {
switch (hookStatus) {
case 'idle':
case 'completed':
@ -239,20 +263,26 @@ export default function ThreadPage({
setAgentStatus('running');
break;
}
}, [setAgentStatus, setAgentRunId, setAutoOpenedPanel]);
},
[setAgentStatus, setAgentRunId, setAutoOpenedPanel],
);
const handleStreamError = useCallback((errorMessage: string) => {
console.error(`[PAGE] Stream hook error: ${errorMessage}`);
if (
!errorMessage.toLowerCase().includes('not found') &&
!errorMessage.toLowerCase().includes('agent run is not running')
) {
toast.error(`Stream Error: ${errorMessage}`);
const lower = errorMessage.toLowerCase();
const isExpected =
lower.includes('not found') || lower.includes('agent run is not running');
// Downgrade log level for expected/benign cases (opening old conversations)
if (isExpected) {
console.info(`[PAGE] Stream skipped for inactive run: ${errorMessage}`);
return;
}
console.error(`[PAGE] Stream hook error: ${errorMessage}`);
toast.error(`Stream Error: ${errorMessage}`);
}, []);
const handleStreamClose = useCallback(() => {
}, []);
const handleStreamClose = useCallback(() => {}, []);
const {
status: streamHookStatus,
@ -299,39 +329,50 @@ export default function ThreadPage({
try {
const messagePromise = addUserMessageMutation.mutateAsync({
threadId,
message
message,
});
const agentPromise = startAgentMutation.mutateAsync({
threadId,
options: {
...options,
agent_id: selectedAgentId
}
agent_id: selectedAgentId,
},
});
const results = await Promise.allSettled([messagePromise, agentPromise]);
const results = await Promise.allSettled([
messagePromise,
agentPromise,
]);
if (results[0].status === 'rejected') {
const reason = results[0].reason;
console.error("Failed to send message:", reason);
throw new Error(`Failed to send message: ${reason?.message || reason}`);
console.error('Failed to send message:', reason);
throw new Error(
`Failed to send message: ${reason?.message || reason}`,
);
}
if (results[1].status === 'rejected') {
const error = results[1].reason;
console.error("Failed to start agent:", error);
console.error('Failed to start agent:', error);
if (error instanceof BillingError) {
setBillingData({
currentUsage: error.detail.currentUsage as number | undefined,
limit: error.detail.limit as number | undefined,
message: error.detail.message || 'Monthly usage limit reached. Please upgrade.',
accountId: null
message:
error.detail.message ||
'Monthly usage limit reached. Please upgrade.',
accountId: null,
});
setShowBillingAlert(true);
setMessages(prev => prev.filter(m => m.message_id !== optimisticUserMessage.message_id));
setMessages((prev) =>
prev.filter(
(m) => m.message_id !== optimisticUserMessage.message_id,
),
);
return;
}
@ -344,7 +385,11 @@ export default function ThreadPage({
});
setShowAgentLimitDialog(true);
setMessages(prev => prev.filter(m => m.message_id !== optimisticUserMessage.message_id));
setMessages((prev) =>
prev.filter(
(m) => m.message_id !== optimisticUserMessage.message_id,
),
);
return;
}
@ -354,10 +399,12 @@ export default function ThreadPage({
const agentResult = results[1].value;
setUserInitiatedRun(true);
setAgentRunId(agentResult.agent_run_id);
} catch (err) {
console.error('Error sending message or starting agent:', err);
if (!(err instanceof BillingError) && !(err instanceof AgentRunLimitError)) {
if (
!(err instanceof BillingError) &&
!(err instanceof AgentRunLimitError)
) {
toast.error(err instanceof Error ? err.message : 'Operation failed');
}
setMessages((prev) =>
@ -367,7 +414,16 @@ export default function ThreadPage({
setIsSending(false);
}
},
[threadId, project?.account_id, addUserMessageMutation, startAgentMutation, setMessages, setBillingData, setShowBillingAlert, setAgentRunId],
[
threadId,
project?.account_id,
addUserMessageMutation,
startAgentMutation,
setMessages,
setBillingData,
setShowBillingAlert,
setAgentRunId,
],
);
const handleStopAgent = useCallback(async () => {
@ -384,7 +440,8 @@ export default function ThreadPage({
}
}, [stopStreaming, agentRunId, stopAgentMutation, setAgentStatus]);
const handleOpenFileViewer = useCallback((filePath?: string, filePathList?: string[]) => {
const handleOpenFileViewer = useCallback(
(filePath?: string, filePathList?: string[]) => {
if (filePath) {
setFileToView(filePath);
} else {
@ -392,7 +449,9 @@ export default function ThreadPage({
}
setFilePathList(filePathList);
setFileViewerOpen(true);
}, []);
},
[],
);
const toolViewAssistant = useCallback(
(assistantContent?: string, toolContent?: string) => {
@ -404,7 +463,9 @@ export default function ThreadPage({
Assistant Message
</div>
<div className="rounded-md border bg-muted/50 p-3">
<div className="text-xs prose prose-xs dark:prose-invert chat-markdown max-w-none">{assistantContent}</div>
<div className="text-xs prose prose-xs dark:prose-invert chat-markdown max-w-none">
{assistantContent}
</div>
</div>
</div>
);
@ -423,7 +484,8 @@ export default function ThreadPage({
Tool Result
</div>
<div
className={`px-2 py-0.5 rounded-full text-xs ${isSuccess
className={`px-2 py-0.5 rounded-full text-xs ${
isSuccess
? 'bg-green-50 text-green-700 dark:bg-green-900 dark:text-green-300'
: 'bg-red-50 text-red-700 dark:bg-red-900 dark:text-red-300'
}`}
@ -432,7 +494,9 @@ export default function ThreadPage({
</div>
</div>
<div className="rounded-md border bg-muted/50 p-3">
<div className="text-xs prose prose-xs dark:prose-invert chat-markdown max-w-none">{toolContent}</div>
<div className="text-xs prose prose-xs dark:prose-invert chat-markdown max-w-none">
{toolContent}
</div>
</div>
</div>
);
@ -464,28 +528,55 @@ export default function ThreadPage({
}
}
}
}, [initialPanelOpenAttempted, messages, toolCalls, initialLoadCompleted, setIsSidePanelOpen, setCurrentToolIndex, isMobile]);
}, [
initialPanelOpenAttempted,
messages,
toolCalls,
initialLoadCompleted,
setIsSidePanelOpen,
setCurrentToolIndex,
isMobile,
]);
useEffect(() => {
// Start streaming if user initiated a run (don't wait for initialLoadCompleted for first-time users)
if (agentRunId && agentRunId !== currentHookRunId && userInitiatedRun) {
startStreaming(agentRunId);
setUserInitiatedRun(false); // Reset flag after starting
return;
}
// Also start streaming if this is from page load with recent active runs
else if (agentRunId && agentRunId !== currentHookRunId && initialLoadCompleted && !userInitiatedRun) {
// Only auto-start streaming on page load if we know the agent is currently running
if (
agentRunId &&
agentRunId !== currentHookRunId &&
initialLoadCompleted &&
!userInitiatedRun &&
agentStatus === 'running'
) {
startStreaming(agentRunId);
}
}, [agentRunId, startStreaming, currentHookRunId, initialLoadCompleted, userInitiatedRun]);
}, [
agentRunId,
startStreaming,
currentHookRunId,
initialLoadCompleted,
userInitiatedRun,
agentStatus,
]);
// No auto-scroll needed with flex-column-reverse
// No intersection observer needed with flex-column-reverse
useEffect(() => {
if ((streamHookStatus === 'completed' || streamHookStatus === 'stopped' ||
streamHookStatus === 'agent_not_running' || streamHookStatus === 'error') &&
(agentStatus === 'running' || agentStatus === 'connecting')) {
if (
(streamHookStatus === 'completed' ||
streamHookStatus === 'stopped' ||
streamHookStatus === 'agent_not_running' ||
streamHookStatus === 'error') &&
(agentStatus === 'running' || agentStatus === 'connecting')
) {
setAgentStatus('idle');
setAgentRunId(null);
}
@ -531,9 +622,15 @@ export default function ThreadPage({
const hasCheckedUpgradeDialog = useRef(false);
useEffect(() => {
if (initialLoadCompleted && subscriptionData && !hasCheckedUpgradeDialog.current) {
if (
initialLoadCompleted &&
subscriptionData &&
!hasCheckedUpgradeDialog.current
) {
hasCheckedUpgradeDialog.current = true;
const hasSeenUpgradeDialog = localStorage.getItem('suna_upgrade_dialog_displayed');
const hasSeenUpgradeDialog = localStorage.getItem(
'suna_upgrade_dialog_displayed',
);
const isFreeTier = subscriptionStatus === 'no_subscription';
if (!hasSeenUpgradeDialog && isFreeTier && !isLocalMode()) {
setShowUpgradeDialog(true);
@ -576,7 +673,9 @@ export default function ThreadPage({
const scrollContainer = scrollContainerRef.current;
if (scrollContainer) {
scrollContainer.addEventListener('scroll', handleScroll, { passive: true });
scrollContainer.addEventListener('scroll', handleScroll, {
passive: true,
});
// Check initial state
setTimeout(() => handleScroll(), 100);
@ -696,27 +795,36 @@ export default function ThreadPage({
scrollContainerRef={scrollContainerRef}
/>
<div
className={cn(
"fixed bottom-0 z-10 bg-gradient-to-t from-background via-background/90 to-transparent px-4 pt-8",
isSidePanelAnimating ? "" : "transition-all duration-200 ease-in-out",
leftSidebarState === 'expanded' ? 'left-[72px] md:left-[256px]' : 'left-[40px]',
isSidePanelOpen && !isMobile ? 'right-[90%] sm:right-[450px] md:right-[500px] lg:right-[550px] xl:right-[650px]' : 'right-0',
isMobile ? 'left-0 right-0' : ''
)}>
<div className={cn(
"mx-auto",
isMobile ? "w-full" : "max-w-3xl"
)}>
'fixed bottom-0 z-10 bg-gradient-to-t from-background via-background/90 to-transparent px-4 pt-8',
isSidePanelAnimating
? ''
: 'transition-all duration-200 ease-in-out',
leftSidebarState === 'expanded'
? 'left-[72px] md:left-[256px]'
: 'left-[40px]',
isSidePanelOpen && !isMobile
? 'right-[90%] sm:right-[450px] md:right-[500px] lg:right-[550px] xl:right-[650px]'
: 'right-0',
isMobile ? 'left-0 right-0' : '',
)}
>
<div className={cn('mx-auto', isMobile ? 'w-full' : 'max-w-3xl')}>
<ChatInput
value={newMessage}
onChange={setNewMessage}
onSubmit={handleSubmitMessage}
placeholder={`Describe what you need help with...`}
loading={isSending}
disabled={isSending || agentStatus === 'running' || agentStatus === 'connecting'}
isAgentRunning={agentStatus === 'running' || agentStatus === 'connecting'}
disabled={
isSending ||
agentStatus === 'running' ||
agentStatus === 'connecting'
}
isAgentRunning={
agentStatus === 'running' || agentStatus === 'connecting'
}
onStopAgent={handleStopAgent}
autoFocus={!isLoading}
onFileBrowse={handleOpenFileViewer}

View File

@ -127,7 +127,11 @@ export function useAgentStream(
// On thread change, ensure any existing stream is cleaned up to avoid stale subscriptions
useEffect(() => {
const previousThreadId = threadIdRef.current;
if (previousThreadId && previousThreadId !== threadId && streamCleanupRef.current) {
if (
previousThreadId &&
previousThreadId !== threadId &&
streamCleanupRef.current
) {
// Close the existing stream for the previous thread
streamCleanupRef.current();
streamCleanupRef.current = null;
@ -188,14 +192,22 @@ export function useAgentStream(
(finalStatus: string, runId: string | null = agentRunId) => {
if (!isMountedRef.current) return;
console.log(`[useAgentStream] Finalizing stream with status: ${finalStatus}, runId: ${runId}`);
console.log(
`[useAgentStream] Finalizing stream with status: ${finalStatus}, runId: ${runId}`,
);
const currentThreadId = threadIdRef.current; // Get current threadId from ref
const currentSetMessages = setMessagesRef.current; // Get current setMessages from ref
// Only finalize if this is for the current run ID or if no specific run ID is provided
if (runId && currentRunIdRef.current && currentRunIdRef.current !== runId) {
console.log(`[useAgentStream] Ignoring finalization for old run ID ${runId}, current is ${currentRunIdRef.current}`);
if (
runId &&
currentRunIdRef.current &&
currentRunIdRef.current !== runId
) {
console.log(
`[useAgentStream] Ignoring finalization for old run ID ${runId}, current is ${currentRunIdRef.current}`,
);
return;
}
@ -219,17 +231,27 @@ export function useAgentStream(
queryClient.invalidateQueries({ queryKey: agentKeys.lists() });
queryClient.invalidateQueries({ queryKey: ['agent-tools', agentId] });
queryClient.invalidateQueries({ queryKey: ['custom-mcp-tools', agentId] });
queryClient.invalidateQueries({
queryKey: ['custom-mcp-tools', agentId],
});
queryClient.invalidateQueries({ queryKey: composioKeys.mcpServers() });
queryClient.invalidateQueries({ queryKey: composioKeys.profiles.all() });
queryClient.invalidateQueries({ queryKey: composioKeys.profiles.credentials() });
queryClient.invalidateQueries({
queryKey: composioKeys.profiles.all(),
});
queryClient.invalidateQueries({
queryKey: composioKeys.profiles.credentials(),
});
queryClient.invalidateQueries({ queryKey: ['triggers', agentId] });
queryClient.invalidateQueries({ queryKey: workflowKeys.agent(agentId) });
queryClient.invalidateQueries({
queryKey: workflowKeys.agent(agentId),
});
queryClient.invalidateQueries({ queryKey: knowledgeBaseKeys.agent(agentId) });
queryClient.invalidateQueries({
queryKey: knowledgeBaseKeys.agent(agentId),
});
}
if (
@ -238,8 +260,7 @@ export function useAgentStream(
finalStatus === 'stopped' ||
finalStatus === 'agent_not_running')
) {
getAgentStatus(runId).catch((err) => {
});
getAgentStatus(runId).catch((err) => {});
}
},
[agentRunId, updateStatus, agentId, queryClient],
@ -278,7 +299,10 @@ export function useAgentStream(
try {
const jsonData = JSON.parse(processedData);
if (jsonData.status === 'error') {
console.error('[useAgentStream] Received error status message:', jsonData);
console.error(
'[useAgentStream] Received error status message:',
jsonData,
);
const errorMessage = jsonData.message || 'Unknown error occurred';
setError(errorMessage);
toast.error(errorMessage, { duration: 15000 });
@ -290,7 +314,10 @@ export function useAgentStream(
}
// --- Process JSON messages ---
const message = safeJsonParse(processedData, null) as UnifiedMessage | null;
const message = safeJsonParse(
processedData,
null,
) as UnifiedMessage | null;
if (!message) {
console.warn(
'[useAgentStream] Failed to parse streamed message:',
@ -408,11 +435,18 @@ export function useAgentStream(
errorMessage = 'Stream connection error';
}
const lower = errorMessage.toLowerCase();
const isExpected =
lower.includes('not found') || lower.includes('not running');
if (isExpected) {
console.info('[useAgentStream] Streaming skipped/ended:', errorMessage);
} else {
console.error('[useAgentStream] Streaming error:', errorMessage, err);
setError(errorMessage);
// Show error toast with longer duration
toast.error(errorMessage, { duration: 15000 });
}
const runId = currentRunIdRef.current;
if (!runId) {
@ -422,7 +456,6 @@ export function useAgentStream(
finalizeStream('error'); // Finalize with generic error if no runId
return;
}
},
[finalizeStream],
);
@ -431,7 +464,9 @@ export function useAgentStream(
if (!isMountedRef.current) return;
const runId = currentRunIdRef.current;
console.log(`[useAgentStream] Stream closed for run ID: ${runId}, status: ${status}`);
console.log(
`[useAgentStream] Stream closed for run ID: ${runId}, status: ${status}`,
);
if (!runId) {
console.warn('[useAgentStream] Stream closed but no active agentRunId.');
@ -460,11 +495,15 @@ export function useAgentStream(
// Check if this is still the current run ID
if (currentRunIdRef.current !== runId) {
console.log(`[useAgentStream] Run ID changed during status check in handleStreamClose, ignoring`);
console.log(
`[useAgentStream] Run ID changed during status check in handleStreamClose, ignoring`,
);
return;
}
console.log(`[useAgentStream] Final status for run ID ${runId}: ${agentStatus.status}`);
console.log(
`[useAgentStream] Final status for run ID ${runId}: ${agentStatus.status}`,
);
if (agentStatus.status === 'running') {
setError('Stream closed unexpectedly while agent was running.');
@ -481,7 +520,9 @@ export function useAgentStream(
// Check if this is still the current run ID
if (currentRunIdRef.current !== runId) {
console.log(`[useAgentStream] Run ID changed during error handling in handleStreamClose, ignoring`);
console.log(
`[useAgentStream] Run ID changed during error handling in handleStreamClose, ignoring`,
);
return;
}
@ -550,27 +591,31 @@ export function useAgentStream(
// Check if this is still the current run ID we're trying to start
if (currentRunIdRef.current !== runId) {
console.log(`[useAgentStream] Run ID changed during status check, aborting stream for ${runId}`);
console.log(
`[useAgentStream] Run ID changed during status check, aborting stream for ${runId}`,
);
return;
}
if (agentStatus.status !== 'running') {
console.warn(
`[useAgentStream] Agent run ${runId} is not in running state (status: ${agentStatus.status}). Cannot start stream.`,
// Expected when opening an old conversation; don't surface as error/toast
console.info(
`[useAgentStream] Skip streaming for inactive run ${runId} (status: ${agentStatus.status}).`,
);
// Only set error and finalize if this is still the current run ID
if (currentRunIdRef.current === runId) {
setError(`Agent run is not running (status: ${agentStatus.status})`);
finalizeStream(
mapAgentStatus(agentStatus.status) || 'agent_not_running',
runId,
);
const final =
agentStatus.status === 'completed' ||
agentStatus.status === 'stopped'
? mapAgentStatus(agentStatus.status)
: 'agent_not_running';
finalizeStream(final, runId);
}
return;
}
console.log(`[useAgentStream] Agent run ${runId} is running, creating stream`);
console.log(
`[useAgentStream] Agent run ${runId} is running, creating stream`,
);
// Agent is running, proceed to create the stream
const cleanup = streamAgent(runId, {
@ -593,7 +638,9 @@ export function useAgentStream(
},
});
streamCleanupRef.current = cleanup;
console.log(`[useAgentStream] Stream created successfully for run ID: ${runId}`);
console.log(
`[useAgentStream] Stream created successfully for run ID: ${runId}`,
);
// Status will be updated to 'streaming' by the first message received in handleStreamMessage
// If for some reason no message arrives shortly, verify liveness again to avoid zombie state
@ -606,7 +653,10 @@ export function useAgentStream(
if (!isMountedRef.current) return;
if (currentRunIdRef.current !== runId) return;
if (latest.status !== 'running') {
finalizeStream(mapAgentStatus(latest.status) || 'agent_not_running', runId);
finalizeStream(
mapAgentStatus(latest.status) || 'agent_not_running',
runId,
);
}
} catch {
// ignore
@ -617,22 +667,32 @@ export function useAgentStream(
// Only handle error if this is still the current run ID
if (currentRunIdRef.current !== runId) {
console.log(`[useAgentStream] Error occurred for old run ID ${runId}, ignoring`);
console.log(
`[useAgentStream] Error occurred for old run ID ${runId}, ignoring`,
);
return;
}
const errorMessage = err instanceof Error ? err.message : String(err);
const lower = errorMessage.toLowerCase();
const isExpected =
lower.includes('not found') ||
lower.includes('404') ||
lower.includes('does not exist') ||
lower.includes('not running');
if (isExpected) {
console.info(
`[useAgentStream] Stream not started for ${runId}: ${errorMessage}`,
);
finalizeStream('agent_not_running', runId);
} else {
console.error(
`[useAgentStream] Error initiating stream for ${runId}: ${errorMessage}`,
);
setError(errorMessage);
const isNotFoundError =
errorMessage.includes('not found') ||
errorMessage.includes('404') ||
errorMessage.includes('does not exist');
finalizeStream(isNotFoundError ? 'agent_not_running' : 'error', runId);
finalizeStream('error', runId);
}
}
},
[