Maybe instead of thinking of it in terms of "the replay is finished," it'd be better to think of it in terms of, "we're all caught up on the event stream for now," which avoids the concept of being finished. Having a hook that gets called the first time there are no more unprocessed events in the event log (or even *every* time that's the case) seems like it'd do the trick.
In our application (still on Axon 2) we recently implemented background replays to support zero-downtime query model changes. When we need to, say, add a new column to a query-model table that can't easily be populated from other tables, we create a new version of the table with the new column, initializing it from the existing table when that makes sense. We add a new event listener to populate the new table. We start a replay while the application is running. While the replay is happening, the application continues to maintain the old version of the table and to query it in response to user requests.
When the migration finishes, we set a flag internally. That flag tells the event listeners to stop populating the old table, and tells the application code to switch over to the new one for queries. (We also, at that point, update any reporting views that pointed to the old table, so our business people see the new column only after it's fully populated.) We get rid of the code that accesses the old table and then we drop it from the database.
This has worked really well for us, but it hinges on being able to tell when the replays are caught up to real time, since that's when it's safe to switch over to the new table and stop maintaining the old one without losing data.
Absent an API for this in Axon 3, our event listeners can keep track of which events they've seen, but this does seem like something Axon should make easy to do; application developers like me are more likely to get it wrong in some subtle way if every one of us has to implement this logic from scratch in the application code.