@@ -482,11 +482,16 @@ public static TurnNotification ForCompleted(string? threadId, string status, str
482482
483483 private sealed class TurnTracker
484484 {
485+ private static readonly TimeSpan DeferredCompletionDelay = TimeSpan . FromMilliseconds ( 50 ) ;
485486 private readonly StringBuilder _text = new ( ) ;
486487 private readonly object _lock = new ( ) ;
487488 private readonly List < TurnNotification > _queuedNotifications = new ( ) ;
488489 private string ? _lastError ;
489490 private bool _isPrimed ;
491+ private bool _hasDeferredCompletion ;
492+ private string _deferredCompletionStatus = "unknown" ;
493+ private string ? _deferredCompletionErrorMessage ;
494+ private bool _deferredCompletionScheduled ;
490495
491496 public string ThreadId { get ; }
492497 public string TurnId { get ; }
@@ -565,19 +570,82 @@ private void ApplyNotificationLocked(TurnNotification notification)
565570
566571 _text . Append ( notification . Delta ) ;
567572 Progress ? . Report ( new CodexDelta ( notification . ThreadId ?? ThreadId , TurnId , notification . Delta ) ) ;
573+
574+ // Guard against out-of-order notification timing where completion is observed before
575+ // the first delta for a turn. Finalize once delta text is available.
576+ if ( _hasDeferredCompletion )
577+ {
578+ CompleteTurnLocked ( _deferredCompletionStatus , _deferredCompletionErrorMessage ) ;
579+ }
568580 return ;
569581 }
570582 case TurnNotificationKind . Completed :
571583 {
572- var aggregated = _text . ToString ( ) ;
573584 var finalError = notification . ErrorMessage ?? _lastError ;
574- Completion . TrySetResult ( new CodexTurnResult ( ThreadId , TurnId , notification . Status , finalError , aggregated ) ) ;
585+ if ( _text . Length == 0 &&
586+ string . IsNullOrWhiteSpace ( finalError ) &&
587+ string . Equals ( notification . Status , "completed" , StringComparison . OrdinalIgnoreCase ) )
588+ {
589+ _hasDeferredCompletion = true ;
590+ _deferredCompletionStatus = notification . Status ;
591+ _deferredCompletionErrorMessage = finalError ;
592+ ScheduleDeferredCompletion ( ) ;
593+ return ;
594+ }
595+
596+ CompleteTurnLocked ( notification . Status , finalError ) ;
575597 return ;
576598 }
577599 default :
578600 return ;
579601 }
580602 }
603+
604+ private void CompleteTurnLocked ( string status , string ? errorMessage )
605+ {
606+ if ( Completion . Task . IsCompleted )
607+ {
608+ return ;
609+ }
610+
611+ _hasDeferredCompletion = false ;
612+ var aggregated = _text . ToString ( ) ;
613+ Completion . TrySetResult ( new CodexTurnResult ( ThreadId , TurnId , status , errorMessage , aggregated ) ) ;
614+ }
615+
616+ private void ScheduleDeferredCompletion ( )
617+ {
618+ if ( _deferredCompletionScheduled )
619+ {
620+ return ;
621+ }
622+
623+ _deferredCompletionScheduled = true ;
624+ _ = Task . Run ( async ( ) =>
625+ {
626+ try
627+ {
628+ await Task . Delay ( DeferredCompletionDelay ) ;
629+ lock ( _lock )
630+ {
631+ if ( _hasDeferredCompletion )
632+ {
633+ CompleteTurnLocked ( _deferredCompletionStatus , _deferredCompletionErrorMessage ) ;
634+ }
635+ }
636+ }
637+ catch
638+ {
639+ }
640+ finally
641+ {
642+ lock ( _lock )
643+ {
644+ _deferredCompletionScheduled = false ;
645+ }
646+ }
647+ } ) ;
648+ }
581649 }
582650}
583651
0 commit comments