Why is there an aggregate in this UPDATE plan?
This isn't Halloween protection, it's normal UPDATE semantics.

Check out this warning from the docs on UPDATE:


  The results of an UPDATE statement are undefined if the statement includes a FROM clause that is not specified in such a way that only one value is available for each column occurrence that is updated, that is if the UPDATE statement is not deterministic.


A SET statement doesn't accumulate like you expect it to* - it just updates each qualifying row in the Summary table once.  

Since the join results in duplicate potential values for collected, the optimizer introduced a Stream Aggregate - essentially it groups by the "primary key" of the heap (the row locator, referred to as Bmk1000 in the execution plan) to get just one value for collected in each row.

Looking at the plan XML, you can see that the internal only "ANY" aggregate was used to choose among the possible values of collected and fact:

&lt;ScalarOperator ScalarString="ANY(@Summary.[collected] as [s].[collected])"&gt;
  &lt;Aggregate AggType="ANY" Distinct="false"&gt;
    &lt;ScalarOperator&gt;
      &lt;Identifier&gt;
        &lt;ColumnReference Table="@Summary" Alias="[s]" Column="collected" /&gt;
      &lt;/Identifier&gt;
    &lt;/ScalarOperator&gt;
  &lt;/Aggregate&gt;
&lt;/ScalarOperator&gt;


*this sort of accumulation does work when you add a variable assignment into the mix, although it's still not "supported."  This is referred to as "quirky updates" (reference)