What happens
Every activity that succeeds writes both events back to back, with the same body:
4 ActivityTaskCompleted {"activityId":"01a06147-0ea8-…","result":"ch_000000018"}
5 ActivityCompleted {"activityId":"01a06147-0ea8-…","result":"ch_000000018"}
From Worker/ActivityMessageProcessor.php:127:
$this->eventStore->append(new ActivityTaskCompleted($message->executionId, $message->activityId, $result));
$this->eventStore->append(new ActivityCompleted($message->executionId, $message->activityId, $result));
On the admin timeline they render as two rows a reader cannot tell apart. The first question anyone asks is whether the dashboard is broken.
The distinction is real, and only on the failure path
I traced who reads which:
ActivityCompleted - read by EventStoreHistorySource and ActivityEventJournal, i.e. by replay. Load-bearing.
ActivityTaskCompleted - read by JournalRunHistoryReader only, i.e. by the dashboard.
On failure they carry different things: ActivityTaskFailed has the attempt number, and the second event carries a retry state, and three failed attempts produce three of the first for one of the second. That asymmetry is worth having.
On success there is exactly one attempt that mattered, so the attempt's result is the settled result, and the pair is a literal duplicate - one row of storage and one row of timeline per activity, carrying nothing.
Ask
Either of:
- Skip
ActivityTaskCompleted when the activity succeeded on its first attempt, or when its body would equal the settlement. The observational value is the attempt count, and one attempt is already implied.
- Keep both and let the dashboard fold them, which needs the phase field (companion issue) so the view can tell a settlement from an attempt result without comparing JSON.
I would not merge the two event types: the failure path uses the split, and losing it would cost the "three attempts, one outcome" reading that makes retries legible.
Workaround in use
The view hides the second row when its payload is identical to the row above, and explains why in the expansion. It works, and it compares serialised JSON to do it - which is not something a view should be deciding.
What happens
Every activity that succeeds writes both events back to back, with the same body:
From
Worker/ActivityMessageProcessor.php:127:On the admin timeline they render as two rows a reader cannot tell apart. The first question anyone asks is whether the dashboard is broken.
The distinction is real, and only on the failure path
I traced who reads which:
ActivityCompleted- read byEventStoreHistorySourceandActivityEventJournal, i.e. by replay. Load-bearing.ActivityTaskCompleted- read byJournalRunHistoryReaderonly, i.e. by the dashboard.On failure they carry different things:
ActivityTaskFailedhas the attempt number, and the second event carries a retry state, and three failed attempts produce three of the first for one of the second. That asymmetry is worth having.On success there is exactly one attempt that mattered, so the attempt's result is the settled result, and the pair is a literal duplicate - one row of storage and one row of timeline per activity, carrying nothing.
Ask
Either of:
ActivityTaskCompletedwhen the activity succeeded on its first attempt, or when its body would equal the settlement. The observational value is the attempt count, and one attempt is already implied.I would not merge the two event types: the failure path uses the split, and losing it would cost the "three attempts, one outcome" reading that makes retries legible.
Workaround in use
The view hides the second row when its payload is identical to the row above, and explains why in the expansion. It works, and it compares serialised JSON to do it - which is not something a view should be deciding.