@@ -7930,32 +7930,55 @@ function chatAgent<
79307930 capturedPartialResponse ??
79317931 ( ( await assemblePartialFromChunks ( turnBufferedChunks ) ) as TUIMessage | undefined ) ;
79327932
7933- // Include the partial in the UI-message views too, so customers who
7934- // persist from uiMessages / newUIMessages (not responseMessage) keep
7935- // it as well. Dedup by id against the accumulator to be safe.
7933+ // Build the complete error UI state. A HITL/tool continuation partial
7934+ // reuses an existing assistant id, so replace it in place (like the
7935+ // success path) instead of dropping it as a dup; otherwise append.
7936+ // `erroredWireMessage` was already folded into `erroredUIMessages`
7937+ // above when the pre-run merge hadn't happened.
7938+ const partialIdx =
7939+ partialResponse ?. id != null
7940+ ? erroredUIMessages . findIndex ( ( m ) => m . id === partialResponse ! . id )
7941+ : - 1 ;
7942+ const erroredUIMessagesWithPartial : TUIMessage [ ] = ! partialResponse
7943+ ? erroredUIMessages
7944+ : partialIdx === - 1
7945+ ? [ ...erroredUIMessages , partialResponse ]
7946+ : ( erroredUIMessages . map ( ( m , i ) =>
7947+ i === partialIdx ? partialResponse : m
7948+ ) as TUIMessage [ ] ) ;
7949+
79367950 const erroredNewUIMessages : TUIMessage [ ] = erroredWireMessage
79377951 ? [ erroredWireMessage ]
79387952 : [ ] ;
79397953 if ( partialResponse ) {
79407954 erroredNewUIMessages . push ( partialResponse ) ;
79417955 }
7942- const erroredUIMessagesWithPartial =
7943- partialResponse && ! erroredUIMessages . some ( ( m ) => m . id === partialResponse . id )
7944- ? [ ...erroredUIMessages , partialResponse ]
7945- : erroredUIMessages ;
7946-
7947- // Commit the recovered partial to the canonical accumulator so the
7948- // partial survives past this hook: the run stays alive after an
7949- // error, so the next turn sees it, and the error-path snapshot below
7950- // (the recovery source for non-hydrate apps) persists it for reboot.
7951- // Matches the success path, which accumulates the response. Guard the
7952- // model-message conversion so a secondary failure here can't crash
7953- // the still-alive run.
7954- if ( partialResponse ) {
7955- accumulatedUIMessages = erroredUIMessagesWithPartial as TUIMessage [ ] ;
7956+
7957+ // Commit the complete error state to the canonical accumulator so the
7958+ // errored user message and any recovered partial survive past this
7959+ // hook: the run stays alive after an error, so the next turn sees
7960+ // them, and the error-path snapshot below (the recovery source for
7961+ // non-hydrate apps) persists them for reboot. Matches the success
7962+ // path. Skip when nothing changed so an errored turn never needlessly
7963+ // reconverts. When the only change is an appended partial, push just
7964+ // its model messages to preserve a prior turn's compaction (mirrors
7965+ // the success path's append branch); otherwise reconvert from UI.
7966+ // Guard the conversion so a secondary failure can't crash the run.
7967+ if ( erroredUIMessagesWithPartial !== accumulatedUIMessages ) {
7968+ const onlyAppendedPartial =
7969+ partialResponse != null &&
7970+ partialIdx === - 1 &&
7971+ erroredUIMessages === accumulatedUIMessages ;
7972+ accumulatedUIMessages = erroredUIMessagesWithPartial ;
79567973 locals . set ( chatCurrentUIMessagesKey , accumulatedUIMessages ) ;
79577974 try {
7958- accumulatedMessages = await toModelMessages ( accumulatedUIMessages ) ;
7975+ if ( onlyAppendedPartial ) {
7976+ accumulatedMessages . push (
7977+ ...( await toModelMessages ( [ stripProviderMetadata ( partialResponse ! ) ] ) )
7978+ ) ;
7979+ } else {
7980+ accumulatedMessages = await toModelMessages ( accumulatedUIMessages ) ;
7981+ }
79597982 } catch {
79607983 // Keep the prior model accumulator if conversion fails.
79617984 }
0 commit comments