@@ -85,6 +85,8 @@ const STATE_TO_STATUS: Record<string, ToolCallStatus> = {
8585const DEPLOY_TOOL_NAMES = new Set ( [ 'deploy_api' , 'deploy_chat' , 'deploy_mcp' , 'redeploy' ] )
8686const RECONNECT_TAIL_ERROR =
8787 'Live reconnect failed before the stream finished. The latest response may be incomplete.'
88+ const UNEXPECTED_PROVIDER_ERROR_TAG =
89+ '<mothership-error>{"message":"An unexpected provider error occurred"}</mothership-error>'
8890
8991function mapStoredBlock ( block : TaskStoredContentBlock ) : ContentBlock {
9092 const mapped : ContentBlock = {
@@ -415,6 +417,8 @@ export function useChat(
415417 setIsReconnecting ( false )
416418 setResources ( [ ] )
417419 setActiveResourceId ( null )
420+ setStreamingFile ( null )
421+ streamingFileRef . current = null
418422 setMessageQueue ( [ ] )
419423 } , [ initialChatId , queryClient ] )
420424
@@ -433,6 +437,8 @@ export function useChat(
433437 setIsReconnecting ( false )
434438 setResources ( [ ] )
435439 setActiveResourceId ( null )
440+ setStreamingFile ( null )
441+ streamingFileRef . current = null
436442 setMessageQueue ( [ ] )
437443 } , [ isHomePage ] )
438444
@@ -497,7 +503,6 @@ export function useChat(
497503 }
498504
499505 if ( activeStreamId && ! sendingRef . current ) {
500- abortControllerRef . current ?. abort ( )
501506 const gen = ++ streamGenRef . current
502507 const abortController = new AbortController ( )
503508 abortControllerRef . current = abortController
@@ -508,21 +513,16 @@ export function useChat(
508513 const assistantId = crypto . randomUUID ( )
509514
510515 const reconnect = async ( ) => {
516+ let reconnectFailed = false
511517 try {
512518 const encoder = new TextEncoder ( )
513519
514520 const batchEvents = snapshot ?. events ?? [ ]
515521 const streamStatus = snapshot ?. status ?? ''
516522
517523 if ( batchEvents . length === 0 && streamStatus === 'unknown' ) {
518- const cid = chatIdRef . current
519- if ( cid ) {
520- fetch ( stopPathRef . current , {
521- method : 'POST' ,
522- headers : { 'Content-Type' : 'application/json' } ,
523- body : JSON . stringify ( { chatId : cid , streamId : activeStreamId , content : '' } ) ,
524- } ) . catch ( ( ) => { } )
525- }
524+ reconnectFailed = true
525+ setError ( RECONNECT_TAIL_ERROR )
526526 return
527527 }
528528
@@ -550,6 +550,7 @@ export function useChat(
550550 { signal : abortController . signal }
551551 )
552552 if ( ! sseRes . ok || ! sseRes . body ) {
553+ reconnectFailed = true
553554 logger . warn ( 'SSE tail reconnect returned no readable body' , {
554555 status : sseRes . status ,
555556 streamId : activeStreamId ,
@@ -565,6 +566,7 @@ export function useChat(
565566 }
566567 } catch ( err ) {
567568 if ( ! ( err instanceof Error && err . name === 'AbortError' ) ) {
569+ reconnectFailed = true
568570 logger . warn ( 'SSE tail failed during reconnect' , err )
569571 setError ( RECONNECT_TAIL_ERROR )
570572 }
@@ -578,10 +580,11 @@ export function useChat(
578580 await processSSEStreamRef . current ( combinedStream . getReader ( ) , assistantId , gen )
579581 } catch ( err ) {
580582 if ( err instanceof Error && err . name === 'AbortError' ) return
583+ reconnectFailed = true
581584 } finally {
582585 setIsReconnecting ( false )
583586 if ( streamGenRef . current === gen ) {
584- finalizeRef . current ( )
587+ finalizeRef . current ( reconnectFailed ? { error : true } : undefined )
585588 }
586589 }
587590 }
@@ -619,6 +622,17 @@ export function useChat(
619622 return b
620623 }
621624
625+ const appendInlineErrorTag = ( tag : string ) => {
626+ if ( runningText . includes ( tag ) ) return
627+ const tb = ensureTextBlock ( )
628+ const prefix = runningText . length > 0 && ! runningText . endsWith ( '\n' ) ? '\n' : ''
629+ tb . content = `${ tb . content ?? '' } ${ prefix } ${ tag } `
630+ if ( activeSubagent ) tb . subagent = activeSubagent
631+ runningText += `${ prefix } ${ tag } `
632+ streamingContentRef . current = runningText
633+ flush ( )
634+ }
635+
622636 const isStale = ( ) => expectedGen !== undefined && streamGenRef . current !== expectedGen
623637
624638 const flush = ( ) => {
@@ -644,12 +658,9 @@ export function useChat(
644658
645659 try {
646660 while ( true ) {
647- if ( isStale ( ) ) {
648- reader . cancel ( ) . catch ( ( ) => { } )
649- break
650- }
651661 const { done, value } = await reader . read ( )
652662 if ( done ) break
663+ if ( isStale ( ) ) continue
653664
654665 buffer += decoder . decode ( value , { stream : true } )
655666 const lines = buffer . split ( '\n' )
@@ -1114,14 +1125,11 @@ export function useChat(
11141125 }
11151126 case 'error' : {
11161127 setError ( parsed . error || 'An error occurred' )
1128+ appendInlineErrorTag ( UNEXPECTED_PROVIDER_ERROR_TAG )
11171129 break
11181130 }
11191131 }
11201132 }
1121- if ( isStale ( ) ) {
1122- reader . cancel ( ) . catch ( ( ) => { } )
1123- break
1124- }
11251133 }
11261134 } finally {
11271135 if ( streamReaderRef . current === reader ) {
@@ -1500,7 +1508,6 @@ export function useChat(
15001508
15011509 useEffect ( ( ) => {
15021510 return ( ) => {
1503- streamReaderRef . current ?. cancel ( ) . catch ( ( ) => { } )
15041511 streamReaderRef . current = null
15051512 abortControllerRef . current = null
15061513 streamGenRef . current ++
0 commit comments