@@ -36,7 +36,6 @@ const hiddenToolNames = new Set<ToolName | 'spawn_agent_inline'>([
3636 'spawn_agents' ,
3737] )
3838
39- const STREAM_INACTIVITY_TIMEOUT_MS = 15_000
4039const MAX_RETRIES_PER_MESSAGE = 3
4140const RETRY_BACKOFF_BASE_DELAY_MS = 1_000
4241const RETRY_BACKOFF_MAX_DELAY_MS = 8_000
@@ -360,10 +359,6 @@ export const useSendMessage = ({
360359 const retryAttemptsRef = useRef < Record < string , number > > ( { } )
361360 const retryInFlightRef = useRef ( false )
362361 const retryBackoffDelayRef = useRef ( RETRY_BACKOFF_BASE_DELAY_MS )
363- const streamInactivityTimerRef = useRef < ReturnType < typeof setTimeout > | null > (
364- null ,
365- )
366- const streamInactivityTriggeredRef = useRef ( false )
367362 const streamHadOutputRef = useRef ( false )
368363 const currentRunContextRef = useRef < {
369364 userMessageId : string
@@ -597,52 +592,16 @@ export const useSendMessage = ({
597592 }
598593 } , [ ] )
599594
600- const clearStreamInactivityTimer = useCallback ( ( ) => {
601- if ( streamInactivityTimerRef . current ) {
602- clearTimeout ( streamInactivityTimerRef . current )
603- streamInactivityTimerRef . current = null
604- }
605- } , [ ] )
606-
607- const handleStreamInactivityTimeout = useCallback ( ( ) => {
608- clearStreamInactivityTimer ( )
609- const context = currentRunContextRef . current
610- if ( ! context ) {
611- return
612- }
613- streamInactivityTriggeredRef . current = true
614- schedulePendingRetry ( {
615- ...context ,
616- note : 'Timed out…' ,
617- } )
618- const controller = abortControllerRef . current
619- if ( controller && ! controller . signal . aborted ) {
620- controller . abort ( new Error ( 'Stream inactivity timeout' ) )
621- }
622- } , [ abortControllerRef , clearStreamInactivityTimer , schedulePendingRetry ] )
623-
624- const refreshStreamInactivityTimer = useCallback ( ( ) => {
625- clearStreamInactivityTimer ( )
626- if ( ! currentRunContextRef . current ) {
627- return
628- }
629- streamInactivityTimerRef . current = setTimeout (
630- handleStreamInactivityTimeout ,
631- STREAM_INACTIVITY_TIMEOUT_MS ,
632- )
633- } , [ clearStreamInactivityTimer , handleStreamInactivityTimeout ] )
634-
635595 useEffect ( ( ) => {
636596 return ( ) => {
637597 if ( flushTimeoutRef . current ) {
638598 clearTimeout ( flushTimeoutRef . current )
639599 flushTimeoutRef . current = null
640600 }
641- clearStreamInactivityTimer ( )
642601 currentRunContextRef . current = null
643602 flushPendingUpdates ( )
644603 }
645- } , [ clearStreamInactivityTimer , flushPendingUpdates ] )
604+ } , [ flushPendingUpdates ] )
646605
647606 const sendMessage = useCallback < SendMessageFn > (
648607 async ( params : ParamsOf < SendMessageFn > ) => {
@@ -1119,30 +1078,37 @@ export const useSendMessage = ({
11191078 updateChainInProgress ( true )
11201079 let hasReceivedContent = false
11211080 let actualCredits : number | undefined = undefined
1081+ let settled = false
11221082
11231083 streamHadOutputRef . current = false
11241084
11251085 const abortController = new AbortController ( )
11261086 abortControllerRef . current = abortController
1087+
1088+ // Abort listener for immediate UI cleanup.
1089+ // Note: With shared controller, both SDK (timeout) and user (Ctrl+C) can abort.
1090+ // This listener only updates UI state - the error handler manages retries.
11271091 abortController . signal . addEventListener ( 'abort' , ( ) => {
1128- clearStreamInactivityTimer ( )
1129- currentRunContextRef . current = null
1092+ if ( settled ) {
1093+ return
1094+ }
1095+ // Update UI immediately for responsive feedback
11301096 streamHadOutputRef . current = false
11311097 setStreamStatus ( 'idle' )
1132- setCanProcessQueue ( false )
11331098 updateChainInProgress ( false )
11341099 timerController . stop ( 'aborted' )
11351100
1136- markAiMessageInterrupted ( aiMessageId )
1101+ // Note: We intentionally do NOT:
1102+ // - Set canProcessQueue = false (would block retries)
1103+ // - Clear currentRunContextRef (error handler needs it)
1104+ // - Mark message as interrupted (error handler handles it)
11371105 } )
11381106
11391107 currentRunContextRef . current = {
11401108 userMessageId,
11411109 content,
11421110 agentMode,
11431111 }
1144- streamInactivityTriggeredRef . current = false
1145- refreshStreamInactivityTimer ( )
11461112
11471113 try {
11481114 // Load local agent definitions from .agents directory
@@ -1166,12 +1132,11 @@ export const useSendMessage = ({
11661132 agent : selectedAgentDefinition ?? agentId ?? fallbackAgent ,
11671133 prompt : content ,
11681134 previousRun : previousRunStateRef . current ?? undefined ,
1169- signal : abortController . signal ,
1135+ abortController : abortController ,
11701136 agentDefinitions : agentDefinitions ,
11711137 maxAgentSteps : 40 ,
11721138
11731139 handleStreamChunk : ( event ) => {
1174- refreshStreamInactivityTimer ( )
11751140 if ( ! streamHadOutputRef . current ) {
11761141 streamHadOutputRef . current = true
11771142 }
@@ -1243,7 +1208,6 @@ export const useSendMessage = ({
12431208 { errorMessage : event . message } ,
12441209 'SDK error event received' ,
12451210 )
1246- clearStreamInactivityTimer ( )
12471211 currentRunContextRef . current = null
12481212 // Stop streaming and update UI
12491213 setStreamStatus ( 'idle' )
@@ -1300,8 +1264,6 @@ export const useSendMessage = ({
13001264
13011265 if ( typeof text !== 'string' || ! text ) return
13021266
1303- refreshStreamInactivityTimer ( )
1304-
13051267 // Track if main agent (no agentId) started streaming
13061268 if ( ! hasReceivedContent && ! event . agentId ) {
13071269 hasReceivedContent = true
@@ -1946,7 +1908,6 @@ export const useSendMessage = ({
19461908 } )
19471909
19481910 if ( ! runState . output || runState . output . type === 'error' ) {
1949- clearStreamInactivityTimer ( )
19501911 currentRunContextRef . current = null
19511912 streamHadOutputRef . current = false
19521913 setCanProcessQueue ( false )
@@ -1981,7 +1942,6 @@ export const useSendMessage = ({
19811942 return
19821943 }
19831944
1984- clearStreamInactivityTimer ( )
19851945 currentRunContextRef . current = null
19861946 streamHadOutputRef . current = false
19871947 setStreamStatus ( 'idle' )
@@ -2020,13 +1980,14 @@ export const useSendMessage = ({
20201980 }
20211981 } ) ,
20221982 )
1983+ settled = true
20231984 delete retryAttemptsRef . current [ userMessageId ]
20241985 } catch ( error ) {
1986+ settled = true
20251987 logger . error (
20261988 { error : getErrorObject ( error ) } ,
20271989 'SDK client.run() failed' ,
20281990 )
2029- clearStreamInactivityTimer ( )
20301991 currentRunContextRef . current = null
20311992 streamHadOutputRef . current = false
20321993 setStreamStatus ( 'idle' )
@@ -2036,10 +1997,10 @@ export const useSendMessage = ({
20361997
20371998 const errorMessage =
20381999 error instanceof Error ? error . message : 'Unknown error occurred'
2039- const timedOutDueToCli = streamInactivityTriggeredRef . current
2040- if ( ! timedOutDueToCli ) {
2041- markAiMessageInterrupted ( aiMessageId )
2042- }
2000+
2001+ // Mark message as interrupted when an error occurs
2002+ markAiMessageInterrupted ( aiMessageId )
2003+
20432004 applyMessageUpdate ( ( prev ) =>
20442005 prev . map ( ( msg ) => {
20452006 if ( msg . id !== aiMessageId ) {
@@ -2067,10 +2028,8 @@ export const useSendMessage = ({
20672028 userMessageId in pendingRetriesRef . current
20682029 const timedOutDueToSdk =
20692030 isNetworkError ( error ) && Boolean ( error . streamTimedOut )
2070- streamInactivityTriggeredRef . current = false
20712031
20722032 const shouldRetryError =
2073- timedOutDueToCli ||
20742033 timedOutDueToSdk ||
20752034 ! isConnectedRef . current ||
20762035 isRetryableError ( error )
@@ -2091,7 +2050,7 @@ export const useSendMessage = ({
20912050 if ( ! pendingAlreadyScheduled ) {
20922051 const note = ! isConnectedRef . current
20932052 ? 'Waiting for connection…'
2094- : timedOutDueToCli || timedOutDueToSdk
2053+ : timedOutDueToSdk
20952054 ? 'Timed out…'
20962055 : 'Stream interrupted'
20972056
@@ -2133,8 +2092,6 @@ export const useSendMessage = ({
21332092 resumeQueue ,
21342093 clearPendingRetryForMessage ,
21352094 schedulePendingRetry ,
2136- clearStreamInactivityTimer ,
2137- refreshStreamInactivityTimer ,
21382095 isConnectedRef ,
21392096 markAiMessageInterrupted ,
21402097 markUserMessageFailed ,
0 commit comments