@@ -348,13 +348,25 @@ function assistantGroupHasReplyText(group: MessageGroup): boolean {
348348 return group . messages . some ( ( { message } ) => Boolean ( extractTextCached ( message ) ?. trim ( ) ) ) ;
349349}
350350
351+ function assistantGroupIsEmptyBoundary ( group : MessageGroup ) : boolean {
352+ if ( ! group . senderLabel ?. trim ( ) ) {
353+ return false ;
354+ }
355+ return group . messages . every ( ( { message } ) => {
356+ const normalized = safeNormalizeMessage ( message ) ;
357+ return Boolean ( normalized && normalized . content . length === 0 && ! normalized . replyTarget ) ;
358+ } ) ;
359+ }
360+
351361// Stamp each tool group with whether its turn ended in a successful assistant
352362// reply. Codex marks any non-zero exec exit as failed, so a benign internal tool
353363// failure (e.g. a no-match search) must not render as a primary error banner
354364// once a clean reply exists. Backward pass: a user group ends the turn
355365// downstream; an assistant reply marks success for earlier tool groups in the
356- // same turn. turnSucceeded stays undefined for terminal or in-progress failures,
357- // preserving the existing error banner.
366+ // same turn. Empty retained agent groups also end the scan so adjacent
367+ // autonomous turns cannot inherit a later turn's reply. turnSucceeded stays
368+ // undefined for terminal or in-progress failures, preserving the existing error
369+ // banner.
358370function annotateToolTurnOutcome (
359371 items : Array < ChatItem | MessageGroup > ,
360372) : Array < ChatItem | MessageGroup > {
@@ -370,6 +382,8 @@ function annotateToolTurnOutcome(
370382 } else if ( role === "assistant" ) {
371383 if ( assistantGroupHasReplyText ( item ) ) {
372384 sawAssistantReply = true ;
385+ } else if ( assistantGroupIsEmptyBoundary ( item ) ) {
386+ sawAssistantReply = false ;
373387 }
374388 } else if ( role === "tool" ) {
375389 item . turnSucceeded = sawAssistantReply ;
0 commit comments