You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2026/04/27 07:35:11 UTC

(causeway) 01/01: CAUSEWAY-3989: forwardport of replay managers v2->v4

This is an automated email from the ASF dual-hosted git repository.

andi-huber pushed a commit to branch 3989-replay.fwport
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 246d56bcb054ab978a7ad00753a6350c4b544638
Author: andi-huber <ah...@apache.org>
AuthorDate: Mon Apr 27 09:34:59 2026 +0200

    CAUSEWAY-3989: forwardport of replay managers v2->v4
---
 .../applib/CausewayModuleExtCommandLogApplib.java  |  34 +-
 .../commandlog/applib/app/CommandLogMenu.java      |  36 +-
 .../commandlog/applib/dom/CommandLogEntry.java     |   4 +-
 .../applib/dom/CommandLogEntryRepository.java      |  24 +-
 .../dom/CommandLogEntryRepositoryAbstract.java     |  73 ++--
 .../commandlog/applib/dom/ReplayState.java         |  35 +-
 ...ExportManager#exported.columnOrder.fallback.txt |   6 +
 ...Manager#notYetExported.columnOrder.fallback.txt |   6 +
 .../applib/dom/replay/CommandExportManager.java    | 297 ++++++++++++-----
 .../CommandExportManager.layout.fallback.xml       |  12 +-
 ...anager#pendingOrFailed.columnOrder.fallback.txt |   6 +
 ...er#succeededOrExcluded.columnOrder.fallback.txt |   6 +
 .../applib/dom/replay/CommandReplayManager.java    | 371 ++++++++++++++++-----
 .../CommandReplayManager.layout.fallback.xml       |  12 +-
 .../applib/dom/replay/ReplayContext.java           |   6 +-
 .../ReplayableCommand.columnOrder.fallback.txt     |   6 +
 .../applib/dom/replay/ReplayableCommand.java       | 224 ++++++-------
 .../replay/ReplayableCommand.layout.fallback.xml   |  36 +-
 ...yContext.java => ReplayableCommand_delete.java} |  39 ++-
 .../ReplayableCommand_excludeFromReplay.java       |  54 +++
 .../replay/ReplayableCommand_makeExportable.java   |  55 +++
 .../ReplayableCommand_openCommandLogEntry.java     |  54 +++
 .../replay/ReplayableCommand_replayOrRetry.java    |  57 ++++
 .../applib/dom/replay/TimestampMarshallUtil.java   |  48 +++
 .../integtest/CommandLog_IntegTestAbstract.java    |   5 +-
 .../commandlog/jpa/dom/CommandLogEntry.java        |  14 +-
 26 files changed, 1156 insertions(+), 364 deletions(-)

diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/CausewayModuleExtCommandLogApplib.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/CausewayModuleExtCommandLogApplib.java
index e6a467b836a..8cdf6f7877e 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/CausewayModuleExtCommandLogApplib.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/CausewayModuleExtCommandLogApplib.java
@@ -22,9 +22,11 @@
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 
+import org.apache.causeway.applib.services.clock.ClockService;
 import org.apache.causeway.applib.services.command.CommandExecutorService;
 import org.apache.causeway.applib.services.iactn.InteractionService;
 import org.apache.causeway.applib.services.repository.RepositoryService;
+import org.apache.causeway.applib.services.xactn.TransactionService;
 import org.apache.causeway.core.config.util.SpringProfileUtil;
 import org.apache.causeway.extensions.commandlog.applib.app.CommandLogMenu;
 import org.apache.causeway.extensions.commandlog.applib.contributions.HasInteractionId_commandLogEntry;
@@ -36,8 +38,14 @@
 import org.apache.causeway.extensions.commandlog.applib.dom.mixins.CommandLogEntry_childCommands;
 import org.apache.causeway.extensions.commandlog.applib.dom.mixins.CommandLogEntry_openResultObject;
 import org.apache.causeway.extensions.commandlog.applib.dom.mixins.CommandLogEntry_siblingCommands;
+import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager;
 import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandReplayManager;
 import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayContext;
+import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_delete;
+import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_excludeFromReplay;
+import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_makeExportable;
+import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_openCommandLogEntry;
+import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_replayOrRetry;
 import org.apache.causeway.extensions.commandlog.applib.fakescheduler.FakeScheduler;
 import org.apache.causeway.extensions.commandlog.applib.job.BackgroundCommandsJobControl;
 import org.apache.causeway.extensions.commandlog.applib.job.RunBackgroundCommandsJob;
@@ -59,6 +67,24 @@
         CommandLogEntry_childCommands.class,
         CommandLogEntry_openResultObject.class,
         CommandLogEntry_siblingCommands.class,
+        ReplayableCommand_makeExportable.class,
+        ReplayableCommand_openCommandLogEntry.class,
+        ReplayableCommand_replayOrRetry.class,
+        ReplayableCommand_excludeFromReplay.class,
+        ReplayableCommand_delete.class,
+        CommandExportManager.changeSince.class,
+        CommandExportManager.previousHour.class,
+        CommandExportManager.nextHour.class,
+        CommandExportManager.exportSelected.class,
+        CommandExportManager.makeSelectedExportable.class,
+        CommandReplayManager.changeSince.class,
+        CommandExportManager.previousHour.class,
+        CommandReplayManager.nextHour.class,
+        CommandReplayManager.importCommands.class,
+        CommandReplayManager.replayOrRetrySelected.class,
+        CommandReplayManager.excludeSelectedFromReplay.class,
+        CommandReplayManager.deleteSelectedSucceededOrExcluded.class,
+        CommandReplayManager.deleteSelectedPendingOrFailed.class,
 
         // @Component's
         RunBackgroundCommandsJob.class,
@@ -119,10 +145,12 @@ public static void honorSystemEnvironment() {
     @Bean ReplayContext replayContext(
             final RepositoryService repositoryService,
             final InteractionService interactionService,
+            final TransactionService transactionService,
             final CommandLogEntryRepository commandLogEntryRepository,
-            final CommandExecutorService commandExecutorService) {
-        return new ReplayContext(repositoryService, interactionService,
-                commandLogEntryRepository, commandExecutorService);
+            final CommandExecutorService commandExecutorService,
+            final ClockService clockService) {
+        return new ReplayContext(repositoryService, interactionService, transactionService,
+                commandLogEntryRepository, commandExecutorService, clockService);
     }
 
 }
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/app/CommandLogMenu.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/app/CommandLogMenu.java
index 0e33e4e7c57..8dafea89645 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/app/CommandLogMenu.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/app/CommandLogMenu.java
@@ -18,8 +18,10 @@
  */
 package org.apache.causeway.extensions.commandlog.applib.app;
 
+import java.sql.Timestamp;
 import java.time.LocalDate;
 import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
 import java.util.List;
 
 import jakarta.inject.Inject;
@@ -32,6 +34,7 @@
 import org.apache.causeway.applib.annotation.DomainService;
 import org.apache.causeway.applib.annotation.DomainServiceLayout;
 import org.apache.causeway.applib.annotation.MemberSupport;
+import org.apache.causeway.applib.annotation.ParameterLayout;
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.annotation.Publishing;
 import org.apache.causeway.applib.annotation.RestrictTo;
@@ -156,8 +159,19 @@ public class DomainEvent extends ActionDomainEvent<findAll> { }
     public class exportManager {
         public class DomainEvent extends ActionDomainEvent<exportManager> { }
 
-        @MemberSupport public CommandExportManager act() {
-            return new CommandExportManager(null, replayContext);
+        @MemberSupport public CommandExportManager act(
+                @ParameterLayout(
+                        describedAs = "Limits the commands shown; "
+                            + "only commands since this timestamp are available for export. "
+                            + "Set to a time immediately before the commands to be replayed.")
+                final java.sql.Timestamp since
+        ) {
+            return new CommandExportManager(since, replayContext);
+        }
+
+        @MemberSupport public java.sql.Timestamp defaultSince() {
+            final var now = clockService.getClock().nowAsJavaSqlTimestamp();
+            return truncatedTo(now, ChronoUnit.HOURS);
         }
     }
 
@@ -172,11 +186,25 @@ public class DomainEvent extends ActionDomainEvent<exportManager> { }
     public class replayManager {
         public class DomainEvent extends ActionDomainEvent<replayManager> { }
 
-        @MemberSupport public CommandReplayManager act() {
-            return new CommandReplayManager(null, replayContext);
+        @MemberSupport public CommandReplayManager act(
+                @ParameterLayout(
+                        describedAs = "Limits the commands shown; "
+                            + "only commands since this timestamp are available for replay. "
+                            + "Set to a time immediately before the commands to be replayed.")
+                final java.sql.Timestamp since
+        ) {
+            return new CommandReplayManager(since, replayContext);
+        }
+
+        @MemberSupport public java.sql.Timestamp defaultSince() {
+            final var now = clockService.getClock().nowAsJavaSqlTimestamp();
+            return truncatedTo(now, ChronoUnit.HOURS);
         }
     }
 
+    private static Timestamp truncatedTo(final Timestamp now, final ChronoUnit chronoUnit) {
+        return Timestamp.from(now.toInstant().truncatedTo(chronoUnit));
+    }
 
     private LocalDate now() {
         return clockService.getClock().nowAsLocalDate(ZoneId.systemDefault());
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntry.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntry.java
index efae57536c3..99a167025f0 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntry.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntry.java
@@ -144,7 +144,9 @@ public static class Nq {
          * primary before the production database was restored to the secondary.
          */
         public static final String FIND_MOST_RECENT_COMPLETED           = LOGICAL_TYPE_NAME + ".findMostRecentCompleted";
-        public static final String FIND_BY_REPLAY_STATE                 = LOGICAL_TYPE_NAME + ".findNotYetReplayed";
+        public static final String FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATE  = LOGICAL_TYPE_NAME + ".findForegroundByTimestampAfterAndReplayState";
+        public static final String FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATES = LOGICAL_TYPE_NAME + ".findForegroundByTimestampAfterAndReplayStates";
+
         public static final String FIND_BACKGROUND_AND_NOT_YET_STARTED  = LOGICAL_TYPE_NAME + ".findBackgroundAndNotYetStarted";
         public static final String FIND_RECENT_BACKGROUND_BY_TARGET     = LOGICAL_TYPE_NAME + ".findRecentBackgroundByTarget";
     }
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepository.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepository.java
index d067f446e76..3cc377148c1 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepository.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepository.java
@@ -18,6 +18,7 @@
  */
 package org.apache.causeway.extensions.commandlog.applib.dom;
 
+import java.sql.Timestamp;
 import java.time.LocalDate;
 import java.util.Collections;
 import java.util.List;
@@ -120,6 +121,20 @@ List<CommandLogEntry> findByTargetAndFromAndTo(
      */
     List<CommandLogEntry> findSince(UUID interactionId, Integer batchSize);
 
+    List<CommandLogEntry> findForegroundSinceTimestampAndCanBeExported(Timestamp since);
+
+    List<CommandLogEntry> findForegroundSinceTimestampAndHasBeenExported(Timestamp since);
+
+    /**
+     * Command Replay feature: Can replay or retry.
+     */
+    List<CommandLogEntry> findForegroundSinceTimestampAndWithReplayPendingOrFailed(Timestamp since);
+
+    /**
+     * Command Replay feature: Cannot replay or retry.
+     */
+    List<CommandLogEntry> findSinceAndWithReplayOkOrExcluded(Timestamp since);
+
     /**
      * Returns any persisted commands that have not yet started.
      *
@@ -158,15 +173,6 @@ List<CommandLogEntry> findByTargetAndFromAndTo(
      */
     Optional<CommandLogEntry> findMostRecentCompleted();
 
-    /**
-     * Command Replay feature: Can replay or retry.
-     */
-    List<CommandLogEntry> findReplayPendingOrFailed();
-    /**
-     * Command Replay feature: Cannot replay or retry.
-     */
-    List<CommandLogEntry> findReplaySucceededOrExcluded();
-
     CommandLogEntry saveForReplay(CommandDto dto);
 
     default List<CommandLogEntry> saveForReplay(@Nullable final List<CommandDto> commandDtoList) {
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepositoryAbstract.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepositoryAbstract.java
index d7e8cb391a9..d4656396785 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepositoryAbstract.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepositoryAbstract.java
@@ -332,44 +332,13 @@ public Optional<CommandLogEntry> findMostRecentCompleted() {
                     Query.named(commandLogEntryClass, CommandLogEntry.Nq.FIND_MOST_RECENT_COMPLETED))
         );
     }
-    @Override
-    public List<CommandLogEntry> findReplayPendingOrFailed() {
-        return _Casts.uncheckedCast(
-                repositoryService().allMatches(
-                    Query.named(commandLogEntryClass, CommandLogEntry.Nq.FIND_BY_REPLAY_STATE)
-                        .withParameter("replayState1", ReplayState.PENDING)
-                        .withParameter("replayState2", ReplayState.FAILED))
-        );
-    }
-    /**
-     * Command Replay feature: Cannot replay or retry.
-     */
-    @Override
-    public List<CommandLogEntry> findReplaySucceededOrExcluded() {
-        return _Casts.uncheckedCast(
-                repositoryService().allMatches(
-                    Query.named(commandLogEntryClass, CommandLogEntry.Nq.FIND_BY_REPLAY_STATE)
-                        .withParameter("replayState1", ReplayState.OK)
-                        .withParameter("replayState2", ReplayState.EXCLUDED))
-        );
-    }
 
     @Override
     public C saveForReplay(final CommandDto commandToReplay) {
-
-//TODO why?
-//        if(commandToReplay.getMember().getInteractionType() == InteractionType.ACTION_INVOCATION) {
-//            final MapDto userData = commandToReplay.getUserData();
-//            if (userData == null )
-//                throw new IllegalStateException(String.format(
-//                        "Can only persist action DTOs with additional userData; got: \n%s",
-//                        CommandDtoUtils.dtoMapper().toString(commandToReplay)));
-//        }
-
         final C entity = factoryService.detachedEntity(commandLogEntryClass);
         entity.init(commandToReplay, ReplayState.PENDING, 0);
         entity.setParentInteractionId(null); // n/a for replay
-        entity.setExecuteIn(null); // to be specified later depending on user action
+        entity.setExecuteIn(ExecuteIn.FOREGROUND);  // only ever replay foreground commands
 
         persist(entity);
 
@@ -425,6 +394,46 @@ private List<CommandLogEntry> findSince(
                 : commandJdos;
     }
 
+    @Override
+    public List<CommandLogEntry> findForegroundSinceTimestampAndCanBeExported(final Timestamp since) {
+        return findForegroundSinceTimestampWithState(since, ReplayState.UNDEFINED);
+    }
+
+    @Override
+    public List<CommandLogEntry> findForegroundSinceTimestampAndHasBeenExported(final Timestamp since) {
+        return findForegroundSinceTimestampWithState(since, ReplayState.EXPORTED);
+    }
+
+    @Override
+    public List<CommandLogEntry> findForegroundSinceTimestampAndWithReplayPendingOrFailed(final Timestamp since) {
+        return findForegroundSinceTimestampWithStates(since, ReplayState.PENDING, ReplayState.FAILED);
+    }
+
+    /**
+     * Command Replay feature: Cannot replay or retry.
+     */
+    @Override
+    public List<CommandLogEntry> findSinceAndWithReplayOkOrExcluded(final Timestamp since) {
+        return findForegroundSinceTimestampWithStates(since, ReplayState.OK, ReplayState.EXCLUDED);
+    }
+
+    private List<CommandLogEntry> findForegroundSinceTimestampWithState(final Timestamp from, final ReplayState replayState) {
+        return _Casts.uncheckedCast(
+                repositoryService().allMatches(
+                        Query.named(commandLogEntryClass, CommandLogEntry.Nq.FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATE)
+                                .withParameter("from", from)
+                                .withParameter("replayState", replayState)));
+    }
+
+    private List<CommandLogEntry> findForegroundSinceTimestampWithStates(final Timestamp from, final ReplayState replayState1, final ReplayState replayState2) {
+        return _Casts.uncheckedCast(
+                repositoryService().allMatches(
+                        Query.named(commandLogEntryClass, CommandLogEntry.Nq.FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATES)
+                                .withParameter("from", from)
+                                .withParameter("replayState1", replayState1)
+                                .withParameter("replayState2", replayState2)));
+    }
+
     private RepositoryService repositoryService() {
         return repositoryServiceProvider.get();
     }
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/ReplayState.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/ReplayState.java
index 932dfa7394f..5be703106dd 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/ReplayState.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/ReplayState.java
@@ -54,33 +54,40 @@ public enum ReplayState {
     public boolean isExported() { return this == EXPORTED; }
     public boolean isFailed() { return this == FAILED; }
 
-    public boolean canExport() {
+    public boolean isExportable() {
         return this == ReplayState.UNDEFINED;
     }
 
-    public boolean canReplayOrRetryOrMarkForExclusion() {
+    public boolean isPendingOrFailed() {
         return this == ReplayState.PENDING
                 || this == ReplayState.FAILED;
     }
 
+    public boolean isOkOrExcluded() {
+        return this == ReplayState.OK
+                || this == ReplayState.EXCLUDED;
+    }
+
     // -- NULL SAFE
 
-    public static boolean canExport(final @Nullable ReplayState replayState) {
-        return replayState!=null
-            ? replayState.canExport()
-            : true;
+    public static boolean isExportable(final @Nullable ReplayState replayState) {
+        return replayState == null
+                || replayState.isExportable();
+    }
+
+    public static boolean isExported(final @Nullable ReplayState replayState) {
+        return replayState != null
+                && replayState.isExported();
     }
 
-    public static boolean canReplayOrRetryOrMarkForExclusion(final @Nullable ReplayState replayState) {
-        return replayState!=null
-            ? replayState.canReplayOrRetryOrMarkForExclusion()
-            : false;
+    public static boolean isPendingOrFailed(final @Nullable ReplayState replayState) {
+        return replayState != null
+                && replayState.isPendingOrFailed();
     }
 
-    public static boolean isExported(final ReplayState replayState) {
-        return replayState!=null
-                ? replayState.isExported()
-                : false;
+    public static boolean isOkOrExcluded(final @Nullable ReplayState replayState) {
+        return replayState != null
+                && replayState.isOkOrExcluded();
     }
 
 }
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#exported.columnOrder.fallback.txt b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#exported.columnOrder.fallback.txt
new file mode 100644
index 00000000000..bde91a6f91d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#exported.columnOrder.fallback.txt
@@ -0,0 +1,6 @@
+#interactionId
+timestamp
+targetType
+targetId
+member
+replayState
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#notYetExported.columnOrder.fallback.txt b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#notYetExported.columnOrder.fallback.txt
new file mode 100644
index 00000000000..bde91a6f91d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#notYetExported.columnOrder.fallback.txt
@@ -0,0 +1,6 @@
+#interactionId
+timestamp
+targetType
+targetId
+member
+replayState
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java
index 878f545f81d..bf9e2dbe94f 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java
@@ -18,8 +18,12 @@
  */
 package org.apache.causeway.extensions.commandlog.applib.dom.replay;
 
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.chrono.ChronoZonedDateTime;
 import java.util.List;
 import java.util.Optional;
+import java.util.stream.Collectors;
 
 import jakarta.inject.Inject;
 import jakarta.inject.Named;
@@ -32,7 +36,12 @@
 import org.apache.causeway.applib.annotation.DomainObject;
 import org.apache.causeway.applib.annotation.DomainObjectLayout;
 import org.apache.causeway.applib.annotation.Introspection;
+import org.apache.causeway.applib.annotation.MemberSupport;
 import org.apache.causeway.applib.annotation.ObjectSupport;
+import org.apache.causeway.applib.annotation.Property;
+import org.apache.causeway.applib.annotation.PropertyLayout;
+import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.annotation.RestrictTo;
 import org.apache.causeway.applib.annotation.SemanticsOf;
 import org.apache.causeway.applib.util.schema.CommandDtoUtils;
 import org.apache.causeway.applib.value.Blob;
@@ -43,135 +52,267 @@
 import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
 import org.apache.causeway.extensions.commandlog.applib.dom.ReplayState;
 
+import static org.apache.causeway.extensions.commandlog.applib.dom.replay.TimestampMarshallUtil.fromString;
+
+import lombok.Getter;
+
 @DomainObject(introspection = Introspection.ANNOTATION_REQUIRED)
 @DomainObjectLayout(cssClassFa = "solid share-from-square")
 @Named(CommandExportManager.LOGICAL_TYPE_NAME)
-public record CommandExportManager(
-        ReplayContext replayContext) implements ViewModel {
+public final class CommandExportManager implements ViewModel {
+
+	public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandExportManager";
 
-    public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandExportManager";
+    public static abstract class ActionDomainEvent<T>
+            extends CausewayModuleExtCommandLogApplib.ActionDomainEvent<T> { }
+
+    private ReplayContext replayContext;
 
     @Inject
     public CommandExportManager(
             final String memento,
             final ReplayContext replayContext) {
-        this(replayContext);
+        this(fromString(memento, replayContext.clockService().getClock().nowAsJavaSqlTimestamp()),  replayContext);
+    }
+
+    public CommandExportManager(
+            final java.sql.Timestamp since,
+            final ReplayContext replayContext) {
+        this.since = since;
+        this.replayContext = replayContext;
     }
 
     @ObjectSupport public String title() {
         return "Command Export Manager";
     }
 
-    @Action(semantics = SemanticsOf.IDEMPOTENT_ARE_YOU_SURE)
+
+    @Property
+    @PropertyLayout(describedAs = "Only commands since this timestamp are available for export")
+    @Getter
+    private java.sql.Timestamp since;
+
+    @Action(
+            semantics = SemanticsOf.SAFE,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = previousHour.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "since", sequence = "1",
+            named = "Previous",
+            position = ActionLayout.Position.PANEL,
+            describedAs = "Move back one hour"
+    )
+    public class previousHour {
+        public class DomainEvent extends ActionDomainEvent<previousHour> { }
+
+        @MemberSupport public CommandExportManager act() {
+            return new CommandExportManager(addSeconds(since, -3600), replayContext);
+        }
+    }
+
+    @Action(
+            semantics = SemanticsOf.SAFE,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = nextHour.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "since", sequence = "3",
+            named = "Next",
+            position = ActionLayout.Position.PANEL,
+            describedAs = "Move forward one hour"
+    )
+    public class nextHour {
+        public class DomainEvent extends ActionDomainEvent<nextHour> { }
+        @MemberSupport public CommandExportManager act() {
+            return new CommandExportManager(addSeconds(since, +3600), replayContext);
+        }
+    }
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            semantics = SemanticsOf.SAFE,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = changeSince.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
     @ActionLayout(
-            sequence = "0.1",
-            describedAs = "Deletes all commands, regardless of state (cannot be undone)")
-    public CommandExportManager deleteAll() {
-        commandLogEntryRepository().removeAll();
-        return this;
+            associateWith = "since", sequence = "2",
+            named = "Change",
+            position = ActionLayout.Position.PANEL
+    )
+    public class changeSince {
+        public class DomainEvent extends ActionDomainEvent<nextHour> { }
+        @MemberSupport public CommandExportManager act(final java.sql.Timestamp since) {
+            return new CommandExportManager(since, replayContext);
+        }
+        @MemberSupport public java.sql.Timestamp defaultSince() {
+            return CommandExportManager.this.since;
+        }
     }
 
+    private static Timestamp addSeconds(final Timestamp since, final int secondsToAdd) {
+        return Timestamp.from(since.toInstant().plusSeconds(secondsToAdd));
+    }
+
+
     // -- NOT YET EXPORTED
 
     @Collection
     @CollectionLayout(
-            describedAs = "Commands that can be exported")
+            describedAs = "Commands that can be exported"
+    )
     public List<ReplayableCommand> getNotYetExported() {
-        return commandLogEntryRepository().findAll().stream()
-            .filter(entry->ReplayState.canExport(entry.getReplayState()))
+        return commandLogEntryRepository().findForegroundSinceTimestampAndCanBeExported(since).stream()
             .map(entry->new ReplayableCommand(
                     entry.getInteractionId(),
                     replayContext))
-            .toList();
+            .collect(Collectors.toList());
     }
 
-    @Action(choicesFrom = "notYetExported", semantics = SemanticsOf.NON_IDEMPOTENT)
-    @ActionLayout(associateWith = "notYetExported",
-        sequence = "1.1",
-        cssClassFa = "solid share-from-square",
-        cssClass = "btn-primary",
-        describedAs = "Exports selected Commands as zipped DTOs for import later. "
-                + "(You need to refresh the page to see changed states.)")
-    public Blob exportSelected(
-            final List<ReplayableCommand> selected) {
-
-        var selectedCommandLogEntries = selected.stream()
-            .map(ReplayableCommand::commandLogEntry)
-            .filter(Optional::isPresent)
-            .map(Optional::get)
-            .sorted()
-            .toList();
-
-        var yaml = CommandDtoUtils.toMultiDocYaml(
-            selectedCommandLogEntries.stream()
-                .filter(entry->!ReplayState.isExported(entry.getReplayState()))
-                .map(CommandLogEntry::getCommandDto)
-                .toList());
-
-        var blob = Clob.of("commands.yaml", CommonMimeType.YAML, yaml)
-                .toBlobUtf8()
-                .zip();
-
-        // do this last once we have successfully created the Clob
-        selectedCommandLogEntries.forEach(c->c.setReplayState(ReplayState.EXPORTED));
-
-        return blob;
-    }
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            choicesFrom = "notYetExported",
+            semantics = SemanticsOf.NON_IDEMPOTENT,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = exportSelected.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "notYetExported", sequence = "1.1",
+            cssClassFa = "solid share-from-square",
+            cssClass = "btn-primary",
+            describedAs = "Exports selected Commands as zipped DTOs for import later. "
+                        + "Refresh the page to see changed states."
+    )
+    public class exportSelected {
+        public class DomainEvent extends ActionDomainEvent<exportSelected> { }
 
-    @Action(choicesFrom = "notYetExported")
-    @ActionLayout(associateWith = "notYetExported", sequence = "1.2",
-            describedAs = "Deletes selected Commands (cannot be undone)")
-    public CommandExportManager deleteSelected(final List<ReplayableCommand> selected) {
-        selected.stream()
-            .forEach(ReplayableCommand::delete); // filtered on its own responsibility
-        return this;
+        @MemberSupport public Blob act(
+                final List<ReplayableCommand> selected,
+                final String filenamePrefix,
+                final boolean filenameTimestamp
+                ) {
+
+            var selectedCommandLogEntries = selected.stream()
+                .map(ReplayableCommand::commandLogEntry)
+                .filter(Optional::isPresent)
+                .map(Optional::get)
+                .sorted()
+                .collect(Collectors.toList());
+
+            var yaml = CommandDtoUtils.toYaml(
+                selectedCommandLogEntries.stream()
+                    .filter(entry->!ReplayState.isExported(entry.getReplayState())) // shouldn't be necessary unless a race condition
+                    .map(CommandLogEntry::getCommandDto)
+                    .collect(Collectors.toList()));
+
+            final var replayableCommand = selected.get(0);  // validate ensures there is at least one command
+            final var timestamp = filenameTimestamp
+                    ? replayableCommand.getTimestampIfAny()
+                        .map(ChronoZonedDateTime::toInstant)
+                        .map(Instant::toString)
+                        .map(x -> "." + x.replaceAll("[^A-Za-z0-9._-]", "_"))   // make safe within filename
+                        .orElse("")
+                    : "";
+            final var filename = filenamePrefix + timestamp;
+
+            var blob = Clob.of(filename, CommonMimeType.YAML, yaml)
+                    .toBlobUtf8();
+
+            // do this last once we have successfully created the Clob
+            selectedCommandLogEntries.forEach(c->c.setReplayState(ReplayState.EXPORTED));
+
+            return blob;
+        }
+
+        @MemberSupport public String disableAct() {
+            return getNotYetExported().isEmpty() ? "No commands in collection" : null;
+        }
+
+        @MemberSupport public String defaultFilenamePrefix() {
+            return "commands";
+        }
+
+        @MemberSupport public boolean defaultFilenameTimestamp() {
+            return true;
+        }
+
+        @MemberSupport public String validateSelected(final List<ReplayableCommand> selected) {
+            return selected != null && selected.isEmpty() ? "Select at least one command to export" : null;
+        }
+
+        // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet
+        @MemberSupport
+        public List<ReplayableCommand> choicesSelected() {
+            return getNotYetExported();
+        }
     }
 
+
     // -- EXPORTED
 
     @Collection
-    @CollectionLayout(
-            describedAs = "Commands that were exported")
+    @CollectionLayout(describedAs = "Commands that have been exported")
     public List<ReplayableCommand> getExported() {
-        return commandLogEntryRepository().findAll().stream()
-            .filter(entry->ReplayState.isExported(entry.getReplayState()))
+        return commandLogEntryRepository().findForegroundSinceTimestampAndHasBeenExported(since).stream()
             .map(entry->new ReplayableCommand(
                     entry.getInteractionId(),
                     replayContext))
-            .toList();
+            .collect(Collectors.toList());
     }
 
-    @Action(choicesFrom = "exported")
-    @ActionLayout(associateWith = "exported", sequence = "2.1",
-            describedAs = "Makes selected Commands exportable (again)")
-    public CommandExportManager makeSelectedExportable(final List<ReplayableCommand> selected) {
-        selected.stream()
-            .forEach(ReplayableCommand::makeExportable); // filtered on its own responsibility
-        return this;
-    }
 
-    @Action(choicesFrom = "exported")
-    @ActionLayout(associateWith = "exported", sequence = "2.2",
-            named = "Delete Selected",
-            describedAs = "Deletes selected Commands (cannot be undone)")
-    public CommandExportManager deleteSelected2(final List<ReplayableCommand> selected) {
-        selected.stream()
-            .forEach(ReplayableCommand::delete); // filtered on its own responsibility
-        return this;
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            choicesFrom = "exported",
+            commandPublishing = Publishing.DISABLED,
+            semantics =  SemanticsOf.IDEMPOTENT,
+            domainEvent = makeSelectedExportable.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "exported", sequence = "2.1",
+            describedAs = "Makes selected Commands exportable (again)"
+    )
+    public class makeSelectedExportable {
+        public class DomainEvent extends ActionDomainEvent<makeSelectedExportable> { }
+
+        @MemberSupport
+        public CommandExportManager act(final List<ReplayableCommand> selected) {
+            selected.forEach(ReplayableCommand::makeExportable); // filtered on its own responsibility
+            return CommandExportManager.this;
+        }
+
+        @MemberSupport
+        public String disableAct() {
+            return getExported().isEmpty() ? "No commands in collection" : null;
+        }
+
+        @MemberSupport
+        public String validateSelected(final List<ReplayableCommand> selected) {
+            return selected != null && selected.isEmpty() ? "Select at least one command" : null;
+        }
+
+        // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet
+        @MemberSupport
+        public List<ReplayableCommand> choicesSelected() {
+            return getExported();
+        }
     }
 
+
     // -- VM STATE
 
     @Override
     public String viewModelMemento() {
-        // TODO could use to store filter state
-        return null;
+        return TimestampMarshallUtil.toString(this.since);
     }
 
     // -- HELPER
-
     private CommandLogEntryRepository commandLogEntryRepository() {
         return replayContext.commandLogEntryRepository();
     }
-
 }
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml
index df20c9b73aa..ba0d2da0ef7 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml
@@ -1,9 +1,17 @@
 <?xml version="1.0" encoding="UTF-8"?><bs:grid xmlns:bs="https://causeway.apache.org/applib/layout/grid/bootstrap3" xmlns:lnk="https://causeway.apache.org/applib/layout/links" xmlns:cpt="https://causeway.apache.org/applib/layout/component" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://causeway.apache.org/applib/layout/component https://causeway.apache.org/applib/layout/component/component.xsd https://causeway.apache.org/applib/layout/links https://caus [...]
     <bs:row>
-        <bs:col span="8" unreferencedActions="true">
+        <bs:col span="6" unreferencedActions="true">
             <cpt:domainObject/>
         </bs:col>
-        <bs:col span="4">
+        <bs:col span="3">
+            <cpt:fieldSet name=" " id="since">
+                <cpt:action id="previousHour"/>
+                <cpt:action id="changeSince"/>
+                <cpt:action id="nextHour"/>
+                <cpt:property id="since"/>
+            </cpt:fieldSet>
+        </bs:col>
+        <bs:col span="3">
             <bs:tabGroup>
                 <bs:tab name="Identity">
                     <bs:row>
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#pendingOrFailed.columnOrder.fallback.txt b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#pendingOrFailed.columnOrder.fallback.txt
new file mode 100644
index 00000000000..bde91a6f91d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#pendingOrFailed.columnOrder.fallback.txt
@@ -0,0 +1,6 @@
+#interactionId
+timestamp
+targetType
+targetId
+member
+replayState
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#succeededOrExcluded.columnOrder.fallback.txt b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#succeededOrExcluded.columnOrder.fallback.txt
new file mode 100644
index 00000000000..bde91a6f91d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#succeededOrExcluded.columnOrder.fallback.txt
@@ -0,0 +1,6 @@
+#interactionId
+timestamp
+targetType
+targetId
+member
+replayState
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java
index f734e487f96..d7fdb81a42d 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java
@@ -18,7 +18,9 @@
  */
 package org.apache.causeway.extensions.commandlog.applib.dom.replay;
 
+import java.sql.Timestamp;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import jakarta.inject.Inject;
 import jakarta.inject.Named;
@@ -31,144 +33,360 @@
 import org.apache.causeway.applib.annotation.DomainObject;
 import org.apache.causeway.applib.annotation.DomainObjectLayout;
 import org.apache.causeway.applib.annotation.Introspection;
+import org.apache.causeway.applib.annotation.MemberSupport;
 import org.apache.causeway.applib.annotation.ObjectSupport;
 import org.apache.causeway.applib.annotation.Parameter;
+import org.apache.causeway.applib.annotation.Property;
+import org.apache.causeway.applib.annotation.PropertyLayout;
+import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.annotation.RestrictTo;
 import org.apache.causeway.applib.annotation.SemanticsOf;
 import org.apache.causeway.applib.util.schema.CommandDtoUtils;
 import org.apache.causeway.applib.value.Blob;
-import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
 import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib;
 import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
 import org.apache.causeway.schema.cmd.v2.CommandDto;
 
+import static org.apache.causeway.extensions.commandlog.applib.dom.replay.TimestampMarshallUtil.fromString;
+
+import lombok.Getter;
+
 @DomainObject(introspection = Introspection.ANNOTATION_REQUIRED)
 @DomainObjectLayout(cssClassFa = "solid circle-play")
 @Named(CommandReplayManager.LOGICAL_TYPE_NAME)
-public record CommandReplayManager(
-        ReplayContext replayContext) implements ViewModel {
+public final class CommandReplayManager implements ViewModel {
 
     public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandReplayManager";
 
+    public static abstract class ActionDomainEvent<T>
+            extends CausewayModuleExtCommandLogApplib.ActionDomainEvent<T> { }
+
+    private ReplayContext replayContext;
+
     @Inject
     public CommandReplayManager(
             final String memento,
             final ReplayContext replayContext) {
-        this(replayContext);
+        this(fromString(memento, replayContext.clockService().getClock().nowAsJavaSqlTimestamp()),  replayContext);
+    }
+
+    public CommandReplayManager(
+            final java.sql.Timestamp since,
+            final ReplayContext replayContext) {
+        this.since = since;
+        this.replayContext = replayContext;
     }
 
     @ObjectSupport public String title() {
         return "Command Replay Manager";
     }
 
-    @Action
+
+    @Property
+    @PropertyLayout(describedAs = "Only commands since this timestamp are available for export")
+    @Getter
+    private java.sql.Timestamp since;
+
+    @Action(
+            semantics = SemanticsOf.SAFE,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = previousHour.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
     @ActionLayout(
-            sequence = "0.1",
-            cssClass = "btn-primary",
-            describedAs = "Imports commands from a zipped yaml, then persists them with replayState=PENDING.")
-    public CommandReplayManager importCommands(
-            @Parameter(fileAccept = ".zip")
-            final Blob zippedCommandsYaml) {
+            associateWith = "since", sequence = "1",
+            named = "Previous",
+            position = ActionLayout.Position.PANEL,
+            describedAs = "Move back one hour"
+    )
+    public class previousHour {
+        public class DomainEvent extends ActionDomainEvent<previousHour> { }
 
-        var yamlDs = zippedCommandsYaml.unZip(CommonMimeType.YAML).asDataSource();
+        @MemberSupport public CommandReplayManager act() {
+            return new CommandReplayManager(addSeconds(since, -3600), replayContext);
+        }
+    }
 
-        final List<CommandDto> commandDtos = CommandDtoUtils.fromYaml(yamlDs);
-        commandDtos.forEach(commandLogEntryRepository()::saveForReplay);
+    @Action(
+            semantics = SemanticsOf.SAFE,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = nextHour.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "since", sequence = "3",
+            named = "Next",
+            position = ActionLayout.Position.PANEL,
+            describedAs = "Move forward one hour"
+    )
+    public class nextHour {
+        public class DomainEvent extends ActionDomainEvent<nextHour> { }
+        @MemberSupport public CommandReplayManager act() {
+            return new CommandReplayManager(addSeconds(since, +3600), replayContext);
+        }
+    }
 
-        return this;
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            semantics = SemanticsOf.SAFE,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = changeSince.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "since", sequence = "2",
+            named = "Change",
+            position = ActionLayout.Position.PANEL
+    )
+    public class changeSince {
+        public class DomainEvent extends ActionDomainEvent<nextHour> { }
+        @MemberSupport public CommandReplayManager act(final java.sql.Timestamp since) {
+            return new CommandReplayManager(since, replayContext);
+        }
+        @MemberSupport public java.sql.Timestamp defaultSince() {
+            return CommandReplayManager.this.since;
+        }
     }
 
-    @Action(semantics = SemanticsOf.IDEMPOTENT_ARE_YOU_SURE)
+    private static Timestamp addSeconds(final Timestamp since, final int secondsToAdd) {
+        return Timestamp.from(since.toInstant().plusSeconds(secondsToAdd));
+    }
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            semantics = SemanticsOf.IDEMPOTENT,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = importCommands.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
     @ActionLayout(
-            sequence = "0.2",
-            describedAs = "Deletes all commands, regardless of state (cannot be undone)")
-    public CommandReplayManager deleteAll() {
-        commandLogEntryRepository().removeAll();
-        return this;
+            sequence = "0.1",
+            associateWith = "pendingOrFailed",
+            cssClass = "btn-primary",
+            describedAs = "Imports commands from a zipped yaml, then persists them with replayState=PENDING."
+    )
+    public class importCommands {
+        public class DomainEvent extends ActionDomainEvent<importCommands> { }
+        public CommandReplayManager act(
+                @Parameter(fileAccept = ".yml")
+                final Blob commandsYaml) {
+            var yamlDs = commandsYaml.asDataSource();
+
+            final List<CommandDto> commandDtos = CommandDtoUtils.fromYaml(yamlDs);
+            commandDtos.forEach(commandLogEntryRepository()::saveForReplay);
+
+            return CommandReplayManager.this;
+        }
     }
 
+
     // -- PENDING OR FAILED
 
     @Collection
     @CollectionLayout(
-            describedAs = "Imported Commands that can be either replayed (replayState=PENDING) or retried (when replayState=FAILED)")
+            describedAs = "Imported Commands that can be either replayed (replayState=PENDING) or retried (when replayState=FAILED)"
+    )
     public List<ReplayableCommand> getPendingOrFailed() {
-        return commandLogEntryRepository().findReplayPendingOrFailed().stream()
+        return commandLogEntryRepository().findForegroundSinceTimestampAndWithReplayPendingOrFailed(since).stream()
             .map(entry->new ReplayableCommand(
                     entry.getInteractionId(),
                     replayContext))
-            .toList();
-    }
-
-    @Action(choicesFrom = "pendingOrFailed")
-    @ActionLayout(associateWith = "pendingOrFailed",
-        sequence = "1.1",
-        cssClassFa = "solid circle-play",
-        cssClass = "btn-primary",
-        describedAs = "Executes the list of commands in sequence, after having sorted them by their timestamp. "
-                + "If any of the given commands fails, "
-                + "the surrounding transaction is rolled back and any successful commands are undone). "
-                + "The command, that caused the failure, gets marked as FAILED.")
-    public CommandReplayManager replayOrRetrySelected(final List<ReplayableCommand> selected) {
-        var replayables = selected.stream()
-            .sorted()
-            .toList();
-        for(var replayableCommand : replayables) {
-            var tryReplayOrRetry = replayableCommand.tryReplayOrRetry(); // filtered on its own responsibility
-            if(tryReplayOrRetry.isFailure()) {
-				return this; // stop further execution
-			}
-        }
-        return this;
-    }
-
-    @Action(choicesFrom = "pendingOrFailed")
-    @ActionLayout(associateWith = "pendingOrFailed", sequence = "1.2",
-            describedAs = "Marks selected Commands to be EXCLUDED from replay")
-    public CommandReplayManager excludeSelectedFromReplay(final List<ReplayableCommand> selected) {
-        selected.stream()
-            .forEach(ReplayableCommand::excludeFromReplay); // filtered on its own responsibility
-        return this;
-    }
-
-    @Action(choicesFrom = "pendingOrFailed")
-    @ActionLayout(associateWith = "pendingOrFailed", sequence = "1.3",
-            describedAs = "Deletes selected Commands (cannot be undone)")
-    public CommandReplayManager deleteSelected(final List<ReplayableCommand> selected) {
-        selected.stream()
-            .forEach(ReplayableCommand::delete); // filtered on its own responsibility
-        return this;
+            .collect(Collectors.toList());
+    }
+
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            choicesFrom = "pendingOrFailed",
+            semantics = SemanticsOf.NON_IDEMPOTENT,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = replayOrRetrySelected.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "pendingOrFailed", sequence = "1.1",
+            cssClassFa = "solid circle-play",
+            cssClass = "btn-primary",
+            describedAs = "Executes the list of commands in sequence, after having sorted them by their timestamp. "
+                    + "If any of the given commands fails, "
+                    + "the surrounding transaction is rolled back and any successful commands are undone). "
+                    + "The command, that caused the failure, gets marked as FAILED."
+    )
+    public class replayOrRetrySelected {
+        public class DomainEvent extends ActionDomainEvent<replayOrRetrySelected> { }
+        @MemberSupport public CommandReplayManager act(final List<ReplayableCommand> selected) {
+            var replayables = selected.stream()
+                .sorted()
+                .collect(Collectors.toList());
+            for(var replayableCommand : replayables) {
+                var tryReplayOrRetry = replayableCommand.tryReplayOrRetry(); // filtered on its own responsibility
+                if(tryReplayOrRetry.isFailure())
+                    return CommandReplayManager.this; // stop further execution
+            }
+            return CommandReplayManager.this;
+        }
+
+
+        @MemberSupport
+        public String disableAct() {
+            return getPendingOrFailed().isEmpty() ? "No commands in collection" : null;
+        }
+
+        @MemberSupport
+        public String validateSelected(final List<ReplayableCommand> selected) {
+            return selected != null && selected.isEmpty() ? "Select at least one command" : null;
+        }
+
+        // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet
+        @MemberSupport
+        public List<ReplayableCommand> choicesSelected() {
+            return getPendingOrFailed();
+        }
     }
 
+
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            choicesFrom = "pendingOrFailed",
+            semantics = SemanticsOf.NON_IDEMPOTENT,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = excludeSelectedFromReplay.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "pendingOrFailed", sequence = "1.2",
+            describedAs = "Marks selected Commands to be EXCLUDED from replay"
+    )
+    public class excludeSelectedFromReplay {
+        public class DomainEvent extends ActionDomainEvent<excludeSelectedFromReplay> { }
+        @MemberSupport
+        public CommandReplayManager act(final List<ReplayableCommand> selected) {
+            selected.stream()
+                .forEach(ReplayableCommand::excludeFromReplay); // filtered on its own responsibility
+            return CommandReplayManager.this;
+        }
+
+        @MemberSupport
+        public String disableAct() {
+            return getPendingOrFailed().isEmpty() ? "No commands in collection" : null;
+        }
+
+        @MemberSupport
+        public String validateSelected(final List<ReplayableCommand> selected) {
+            return selected != null && selected.isEmpty() ? "Select at least one command" : null;
+        }
+
+        // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet
+        @MemberSupport
+        public List<ReplayableCommand> choicesSelected() {
+            return getPendingOrFailed();
+        }
+
+    }
+
+
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            choicesFrom = "pendingOrFailed",
+            semantics = SemanticsOf.NON_IDEMPOTENT,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = deleteSelectedPendingOrFailed.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "pendingOrFailed", sequence = "1.3",
+            describedAs = "Deletes selected Commands (cannot be undone)"
+    )
+    public class deleteSelectedPendingOrFailed {
+        public class DomainEvent extends ActionDomainEvent<deleteSelectedPendingOrFailed> { }
+        public CommandReplayManager act(final List<ReplayableCommand> selected) {
+            selected.stream()
+                .forEach(ReplayableCommand::deleteObj); // filtered on its own responsibility
+            return CommandReplayManager.this;
+        }
+
+        @MemberSupport
+        public String disableAct() {
+            return getPendingOrFailed().isEmpty() ? "No commands in collection" : null;
+        }
+
+        @MemberSupport
+        public String validateSelected(final List<ReplayableCommand> selected) {
+            return selected != null && selected.isEmpty() ? "Select at least one command" : null;
+        }
+
+        // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet
+        @MemberSupport
+        public List<ReplayableCommand> choicesSelected() {
+            return getPendingOrFailed();
+        }
+
+    }
+
+
+
     // -- OK OR EXCLUDE
 
     @Collection
     @CollectionLayout(
             describedAs = "Imported Commands that were either replayed with success (replayState=OK) "
-                    + "or marked to be excluded from replay (replayState=EXCLUDE)")
+                    + "or marked to be excluded from replay (replayState=EXCLUDE)"
+    )
     public List<ReplayableCommand> getSucceededOrExcluded() {
-        return commandLogEntryRepository().findReplaySucceededOrExcluded().stream()
+        return commandLogEntryRepository().findSinceAndWithReplayOkOrExcluded(since).stream()
             .map(entry->new ReplayableCommand(
                     entry.getInteractionId(),
                     replayContext))
-            .toList();
+            .collect(Collectors.toList());
     }
 
-    @Action(choicesFrom = "succeededOrExcluded")
-    @ActionLayout(associateWith = "succeededOrExcluded",
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            choicesFrom = "succeededOrExcluded",
+            semantics = SemanticsOf.IDEMPOTENT,
+            domainEvent = deleteSelectedSucceededOrExcluded.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "succeededOrExcluded",
             named = "Delete Selected",
-            describedAs = "Deletes selected Commands (cannot be undone)")
-    public CommandReplayManager deleteSelected2(final List<ReplayableCommand> selected) {
-        selected.stream()
-            .forEach(ReplayableCommand::delete); // filtered on its own responsibility
-        return this;
+            describedAs = "Deletes selected Commands (cannot be undone)"
+    )
+    public class deleteSelectedSucceededOrExcluded {
+        public class DomainEvent extends ActionDomainEvent<deleteSelectedSucceededOrExcluded> { }
+        public CommandReplayManager act(final List<ReplayableCommand> selected) {
+            selected.stream()
+                .forEach(ReplayableCommand::deleteObj); // filtered on its own responsibility
+            return CommandReplayManager.this;
+        }
+
+        @MemberSupport
+        public String disableAct() {
+            return getSucceededOrExcluded().isEmpty() ? "No commands in collection" : null;
+        }
+
+        @MemberSupport
+        public String validateSelected(final List<ReplayableCommand> selected) {
+            return selected != null && selected.isEmpty() ? "Select at least one command" : null;
+        }
+
+        // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet
+        @MemberSupport
+        public List<ReplayableCommand> choicesSelected() {
+            return getSucceededOrExcluded();
+        }
     }
 
+
+
     // -- VM STATE
 
     @Override
     public String viewModelMemento() {
-        // TODO could use to store filter state
-        return null;
+        return TimestampMarshallUtil.toString(this.since);
     }
 
     // -- HELPER
@@ -176,5 +394,4 @@ public String viewModelMemento() {
     private CommandLogEntryRepository commandLogEntryRepository() {
         return replayContext.commandLogEntryRepository();
     }
-
 }
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.layout.fallback.xml b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.layout.fallback.xml
index 30489ca2158..c7aed354e9f 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.layout.fallback.xml
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.layout.fallback.xml
@@ -1,9 +1,17 @@
 <?xml version="1.0" encoding="UTF-8"?><bs:grid xmlns:bs="https://causeway.apache.org/applib/layout/grid/bootstrap3" xmlns:lnk="https://causeway.apache.org/applib/layout/links" xmlns:cpt="https://causeway.apache.org/applib/layout/component" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://causeway.apache.org/applib/layout/component https://causeway.apache.org/applib/layout/component/component.xsd https://causeway.apache.org/applib/layout/links https://caus [...]
     <bs:row>
-        <bs:col span="8" unreferencedActions="true">
+        <bs:col span="6" unreferencedActions="true">
             <cpt:domainObject/>
         </bs:col>
-        <bs:col span="4">
+        <bs:col span="3">
+            <cpt:fieldSet name=" " id="since">
+                <cpt:action id="previousHour"/>
+                <cpt:action id="changeSince"/>
+                <cpt:action id="nextHour"/>
+                <cpt:property id="since"/>
+            </cpt:fieldSet>
+        </bs:col>
+        <bs:col span="3">
             <bs:tabGroup>
                 <bs:tab name="Identity">
                     <bs:row>
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java
index fb65cb4a511..937599bb12a 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java
@@ -18,9 +18,11 @@
  */
 package org.apache.causeway.extensions.commandlog.applib.dom.replay;
 
+import org.apache.causeway.applib.services.clock.ClockService;
 import org.apache.causeway.applib.services.command.CommandExecutorService;
 import org.apache.causeway.applib.services.iactn.InteractionService;
 import org.apache.causeway.applib.services.repository.RepositoryService;
+import org.apache.causeway.applib.services.xactn.TransactionService;
 import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
 
 /**
@@ -29,6 +31,8 @@
 public record ReplayContext(
         RepositoryService repositoryService,
         InteractionService interactionService,
+        TransactionService transactionService,
         CommandLogEntryRepository commandLogEntryRepository,
-        CommandExecutorService commandExecutorService) {
+        CommandExecutorService commandExecutorService,
+        ClockService clockService) {
 }
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.columnOrder.fallback.txt b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.columnOrder.fallback.txt
new file mode 100644
index 00000000000..bde91a6f91d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.columnOrder.fallback.txt
@@ -0,0 +1,6 @@
+#interactionId
+timestamp
+targetType
+targetId
+member
+replayState
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
index 3428266fc05..88ed8da2de2 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
@@ -18,9 +18,10 @@
  */
 package org.apache.causeway.extensions.commandlog.applib.dom.replay;
 
+import java.time.Instant;
 import java.time.ZonedDateTime;
+import java.time.chrono.ChronoZonedDateTime;
 import java.util.Comparator;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.UUID;
 import java.util.concurrent.Executors;
@@ -28,15 +29,15 @@
 import jakarta.inject.Inject;
 import jakarta.inject.Named;
 
+import org.springframework.transaction.annotation.Propagation;
+
 import org.apache.causeway.applib.ViewModel;
-import org.apache.causeway.applib.annotation.Action;
-import org.apache.causeway.applib.annotation.ActionLayout;
 import org.apache.causeway.applib.annotation.DomainObject;
 import org.apache.causeway.applib.annotation.DomainObjectLayout;
 import org.apache.causeway.applib.annotation.Introspection;
 import org.apache.causeway.applib.annotation.LabelPosition;
-import org.apache.causeway.applib.annotation.MemberSupport;
 import org.apache.causeway.applib.annotation.ObjectSupport;
+import org.apache.causeway.applib.annotation.Programmatic;
 import org.apache.causeway.applib.annotation.Property;
 import org.apache.causeway.applib.annotation.PropertyLayout;
 import org.apache.causeway.applib.annotation.Where;
@@ -51,7 +52,6 @@
 import org.apache.causeway.commons.io.YamlUtils;
 import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib;
 import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry;
-import org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn;
 import org.apache.causeway.extensions.commandlog.applib.dom.ReplayState;
 import org.apache.causeway.schema.cmd.v2.CommandDto;
 import org.apache.causeway.schema.cmd.v2.MemberDto;
@@ -60,38 +60,55 @@
 import org.apache.causeway.valuetypes.asciidoc.builder.AsciiDocBuilder;
 import org.apache.causeway.valuetypes.asciidoc.builder.AsciiDocFactory;
 
+import lombok.AllArgsConstructor;
+import lombok.Value;
+import lombok.experimental.Accessors;
+
 /**
  * Viewmodel that wraps a {@link CommandLogEntry}.
  */
-@DomainObject(introspection = Introspection.ENCAPSULATION_ENABLED)
-@DomainObjectLayout(cssClassFa = "terminal")
+@DomainObject(introspection = Introspection.ANNOTATION_REQUIRED)
+@DomainObjectLayout//(cssClassFa = "terminal")
 @Named(ReplayableCommand.LOGICAL_TYPE_NAME)
-public record ReplayableCommand(
-        UUID interactionId,
-        ReplayContext replayContext,
-        ObjectReference<CommandRecord> recordRef) implements ViewModel, Comparable<ReplayableCommand> {
+@AllArgsConstructor
+public final class ReplayableCommand implements ViewModel, Comparable<ReplayableCommand> {
+
+    public static abstract class ActionDomainEvent<T>
+            extends CausewayModuleExtCommandLogApplib.ActionDomainEvent<T> { }
+
+    private final UUID interactionId;
+    @Programmatic
+    public UUID interactionId() { return interactionId; }
+
+	private final ReplayContext replayContext;
+    @Programmatic
+    public ReplayContext replayContext() { return replayContext; }
+
+
+	private final ObjectReference<CommandRecord> recordRef;
+    @Programmatic
+    public ObjectReference<CommandRecord> recordRef() { return recordRef; }
 
     public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".ReplayableCommand";
 
     // decoupled from the underlying entity
-    record CommandRecord(
-            CommandDto commandDto,
-            ReplayState replayState) {
-        CommandRecord {
-            Objects.requireNonNull(commandDto);
-            Objects.requireNonNull(replayState);
-        }
+    @Value @Accessors(fluent = true)
+    final static class CommandRecord {
+
+	    final CommandDto commandDto;
+	    final ReplayState replayState;
+
         boolean canReplayOrRetryOrMarkForExclusion() {
-            return replayState.canReplayOrRetryOrMarkForExclusion();
+            return replayState.isPendingOrFailed();
         }
         public String faQuickIcon() {
-            return switch(replayState) {
+            return switch (replayState) {
                 case UNDEFINED -> "solid terminal .col-indigo";
-                case EXPORTED  -> "solid terminal .col-indigo, solid circle-arrow-right .ov-size-80 .ov-right-45 .ov-bottom-45 .col-dodgerblue";
-                case PENDING   -> "solid terminal .col-indigo, solid circle-pause       .ov-size-80 .ov-right-45 .ov-bottom-45 .col-gold";
-                case OK        -> "solid terminal .col-indigo, solid circle-check       .ov-size-80 .ov-right-45 .ov-bottom-45 .col-green";
-                case FAILED    -> "solid terminal .col-indigo, solid circle-exclamation .ov-size-80 .ov-right-45 .ov-bottom-45 .col-red";
-                case EXCLUDED  -> "solid terminal .col-indigo, solid circle-xmark       .ov-size-80 .ov-right-45 .ov-bottom-45 .col-grey";
+                case EXPORTED -> "solid terminal .col-indigo, solid circle-arrow-right .ov-size-80 .ov-right-45 .ov-bottom-45 .col-dodgerblue";
+                case PENDING -> "solid terminal .col-indigo, solid circle-pause       .ov-size-80 .ov-right-45 .ov-bottom-45 .col-gold";
+                case OK -> "solid terminal .col-indigo, solid circle-check       .ov-size-80 .ov-right-45 .ov-bottom-45 .col-green";
+                case FAILED -> "solid terminal .col-indigo, solid circle-exclamation .ov-size-80 .ov-right-45 .ov-bottom-45 .col-red";
+                case EXCLUDED -> "solid terminal .col-indigo, solid circle-xmark       .ov-size-80 .ov-right-45 .ov-bottom-45 .col-grey";
             };
         }
     }
@@ -110,7 +127,8 @@ public ReplayableCommand(
     }
 
     @ObjectSupport public String title() {
-        return "Replayable Command";
+        final var timestamp = getTimestampIfAny().map(ChronoZonedDateTime::toInstant).map(Instant::toString).map(x -> " @ " + x).orElse("");
+        return getTargetType() + ":" + getTargetId() + " #" + getMember() + timestamp;
     }
 
     @ObjectSupport public ObjectSupport.IconResource icon(final ObjectSupport.IconSize iconSize) {
@@ -125,29 +143,34 @@ public ReplayableCommand(
     @PropertyLayout(
             sequence = "1.1",
             fieldSetId = "details",
-            describedAs = "UUID of the original (replayabel) Command")
+            describedAs = "UUID of the original (replayable) Command")
     public UUID getInteractionId() {
-        return interactionId();
+        return interactionId;
     }
 
     @Property
     @PropertyLayout(
             sequence = "1.2",
             fieldSetId = "details",
-            describedAs = "Timestamp of the original (replayabel) Command")
+            describedAs = "Timestamp of the original (replayable) Command")
     public ZonedDateTime getTimestamp() {
-        return commandRecord()
-            .map(CommandRecord::commandDto)
-            .map(CommandDto::getTimestamp)
-            .map(JavaTimeXMLGregorianCalendarMarshalling::toZonedDateTime)
+        return getTimestampIfAny()
             .orElse(null);
     }
 
+    @Programmatic
+    public Optional<ZonedDateTime> getTimestampIfAny() {
+        return commandRecord()
+                .map(CommandRecord::commandDto)
+                .map(CommandDto::getTimestamp)
+                .map(JavaTimeXMLGregorianCalendarMarshalling::toZonedDateTime);
+    }
+
     @Property
     @PropertyLayout(
             sequence = "2.1",
             fieldSetId = "details",
-            describedAs = "Target Type of the original (replayabel) Command")
+            describedAs = "Target Type of the original (replayable) Command")
     public String getTargetType() {
         return commandRecord()
             .map(CommandRecord::commandDto)
@@ -160,7 +183,7 @@ public String getTargetType() {
     @PropertyLayout(
             sequence = "2.2",
             fieldSetId = "details",
-            describedAs = "Target ID of the original (replayabel) Command")
+            describedAs = "Target ID of the original (replayable) Command")
     public String getTargetId() {
         return commandRecord()
             .map(CommandRecord::commandDto)
@@ -174,7 +197,7 @@ public String getTargetId() {
     @PropertyLayout(
             sequence = "3.1",
             fieldSetId = "details",
-            describedAs = "Replayabel Action or Property, that was executed as captured by the original Command")
+            describedAs = "Replayable Action or Property, that was executed as captured by the original Command")
     public String getMember() {
         return commandRecord()
             .map(CommandRecord::commandDto)
@@ -189,7 +212,7 @@ public String getMember() {
     @PropertyLayout(
             sequence = "4",
             fieldSetId = "details",
-            describedAs = "Replay State of the original (replayabel) Command. "
+            describedAs = "Replay State of the original (replayable) Command. "
                     + "When imported initially is PENDING. "
                     + "Then after replay its either OK or FAILED. "
                     + "Can be manually set to EXCLUDED, which marks it to be ignored for replay.")
@@ -205,7 +228,7 @@ public ReplayState getReplayState() {
             fieldSetId = "dto",
             hidden = Where.ALL_TABLES,
             labelPosition = LabelPosition.NONE,
-            describedAs = "DTO of the original (replayabel) Command")
+            describedAs = "DTO of the original (replayable) Command")
     public AsciiDoc getDto() {
         return commandRecord()
             .map(CommandRecord::commandDto)
@@ -219,96 +242,57 @@ public AsciiDoc getDto() {
 
     // -- ACTIONS
 
-    @Action
-    @ActionLayout(
-            sequence = "0.2",
-            describedAs = "Opens the associated Command Log Entry")
-    public CommandLogEntry openCommandLogEntry() {
-        return commandLogEntry()
-                .orElse(null);
-    }
-
-    @Action
-    @ActionLayout(
-            sequence = "0.1",
-            cssClassFa = "solid circle-play",
-            cssClass = "btn-primary")
-            //hidden = Where.NOWHERE) // show in tables //TODO NPE bug
-    public ReplayableCommand replayOrRetry() {
-        tryReplayOrRetry();
-        return this;
-    }
-    @MemberSupport private String disableReplayOrRetry() {
-        return commandRecord()
-            .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
-            .orElse(false)
-                ? null
-                : "Cannot replay, if neither PENDING nor FAILED";
-    }
 
-    @Action
-    @ActionLayout(
-            //hidden = Where.NOWHERE, // show in tables //TODO NPE bug
-            sequence = "2.1",
-            associateWith = "replayState",
-            describedAs = "Makes Command exportable (again)")
-    public ReplayableCommand makeExportable() {
+    ReplayableCommand makeExportable() {
         if(disableMakeExportable()!=null)
-            return this; // safe guard when called programmatically
+            return this; // safeguard when called programmatically
         commandLogEntry()
-            .filter(commandLogEntry->ReplayState.isExported(commandLogEntry.getReplayState()))
-            .ifPresent(commandLogEntry->{
-                commandLogEntry.setReplayState(ReplayState.UNDEFINED);
-                invalidateCachedRecord();
-            });
+                .filter(commandLogEntry->ReplayState.isExported(commandLogEntry.getReplayState()))
+                .ifPresent(commandLogEntry->{
+                    commandLogEntry.setReplayState(ReplayState.UNDEFINED);
+                    invalidateCachedRecord();
+                });
         return this;
     }
-    @MemberSupport private String disableMakeExportable() {
+    String disableMakeExportable() {
         return commandRecord()
-            .map(rec->ReplayState.isExported(rec.replayState()))
-            .orElse(false)
+                .map(rec->ReplayState.isExported(rec.replayState()))
+                .orElse(false)
                 ? null
                 : "Cannot make exportable, if not EXPORTED";
     }
 
-    @Action
-    @ActionLayout(
-            //hidden = Where.NOWHERE, // show in tables //TODO NPE bug
-            sequence = "2.2",
-            associateWith = "replayState",
-            describedAs = "Marks Command to be EXCLUDED from replay.")
-    public ReplayableCommand excludeFromReplay() {
+
+    ReplayableCommand excludeFromReplay() {
         if(disableExcludeFromReplay()!=null)
-            return this; // safe guard when called programmatically
+            return ReplayableCommand.this; // safeguard when called programmatically
         commandLogEntry()
-            .filter(ReplayableCommand::canReplayOrRetryOrMarkForExclusion)
-            .ifPresent(commandLogEntry->{
-                commandLogEntry.setReplayState(ReplayState.EXCLUDED);
-                invalidateCachedRecord();
-            });
-        return this;
+                .filter(ReplayableCommand::canReplayOrRetryOrMarkForExclusion)
+                .ifPresent(commandLogEntry->{
+                    commandLogEntry.setReplayState(ReplayState.EXCLUDED);
+                    invalidateCachedRecord();
+                });
+        return ReplayableCommand.this;
     }
-    @MemberSupport private String disableExcludeFromReplay() {
+    String disableExcludeFromReplay() {
         return commandRecord()
-            .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
-            .orElse(false)
+                .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
+                .orElse(false)
                 ? null
                 : "Cannot mark for exclusion, if neither PENDING nor FAILED";
     }
 
-    @Action
-    @ActionLayout(
-            sequence = "0.3",
-            //hidden = Where.NOWHERE, // show in tables //TODO NPE bug
-            describedAs = "Deletes the associated Command Log Entry (cannot be undone)")
-    public void delete() {
+
+    @Programmatic
+    void deleteObj() {
         commandLogEntry()
-            .ifPresent(commandLogEntry->{
-                replayContext.repositoryService().remove(commandLogEntry);
-                invalidateCachedRecord();
-            });
+                .ifPresent(commandLogEntry->{
+                    replayContext.repositoryService().remove(commandLogEntry);
+                    invalidateCachedRecord();
+                });
     }
 
+
     // -- EXECUTION ORDER GOVERNED BY TIMESTAMP
 
     private static final Comparator<ReplayableCommand> TIMESTAMP_COMPARATOR =
@@ -334,10 +318,14 @@ Try<ReplayableCommand> tryReplayOrRetry() {
         return commandLogEntry()
             .filter(ReplayableCommand::canReplayOrRetryOrMarkForExclusion)
             .map(commandLogEntry->{
-                commandLogEntry.setExecuteIn(ExecuteIn.FOREGROUND);
-                var tryResultBookmark = replayContext.commandExecutorService().executeCommand(
-                        InteractionContextPolicy.SWITCH_USER_AND_TIME,
-                        commandLogEntry.getCommandDto());
+                final var commandDto = commandLogEntry.getCommandDto();
+                final var tryResultBookmark = replayContext.transactionService().callTransactional(Propagation.REQUIRES_NEW,
+                        () -> {
+                            final var bookmarkTry = replayContext.commandExecutorService().executeCommand(
+                                    InteractionContextPolicy.SWITCH_USER_AND_TIME,
+                                    commandDto);
+                            return bookmarkTry.valueAsNullableElseFail();
+                        });
 
                 // handle the replay outcome
                 tryResultBookmark.accept(
@@ -347,10 +335,17 @@ Try<ReplayableCommand> tryReplayOrRetry() {
                 invalidateCachedRecord();
 
                 return tryResultBookmark
-                        .mapSuccessAsNullable(__->this);
+                        .mapSuccessAsNullable(__ -> this);
             })
             .orElseGet(()->Try.success(null));
     }
+    String disableReplayOrRetry() {
+        return commandRecord()
+                .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
+                .orElse(false)
+                ? null
+                : "Cannot replay, if neither PENDING nor FAILED";
+    }
 
     // -- HELPER
 
@@ -374,7 +369,7 @@ Optional<CommandLogEntry> commandLogEntry() {
     }
 
     private static boolean canReplayOrRetryOrMarkForExclusion(final CommandLogEntry commandLogEntry) {
-        return ReplayState.canReplayOrRetryOrMarkForExclusion(commandLogEntry.getReplayState());
+        return ReplayState.isPendingOrFailed(commandLogEntry.getReplayState());
     }
 
     private void onReplayError(final UUID interactionId, final Throwable ex) {
@@ -383,5 +378,4 @@ private void onReplayError(final UUID interactionId, final Throwable ex) {
                 replayContext.commandLogEntryRepository().findByInteractionId(interactionId)
                     .ifPresent(entry->entry.saveAnalysis(ex.toString()))));
     }
-
-}
+}
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.layout.fallback.xml b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.layout.fallback.xml
index 0b6c5fee4d5..331644bba1a 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.layout.fallback.xml
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.layout.fallback.xml
@@ -1,14 +1,20 @@
 <?xml version="1.0" encoding="UTF-8"?><bs:grid xmlns:bs="https://causeway.apache.org/applib/layout/grid/bootstrap3" xmlns:lnk="https://causeway.apache.org/applib/layout/links" xmlns:cpt="https://causeway.apache.org/applib/layout/component" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://causeway.apache.org/applib/layout/component https://causeway.apache.org/applib/layout/component/component.xsd https://causeway.apache.org/applib/layout/links https://caus [...]
     <bs:row>
-        <bs:col span="8" unreferencedActions="true">
+        <bs:col span="12" unreferencedActions="true">
             <cpt:domainObject/>
         </bs:col>
-        <bs:col span="4">
+    </bs:row>
+    <bs:row>
+        <bs:col span="6">
             <bs:tabGroup>
                 <bs:tab name="Identity">
                     <bs:row>
                         <bs:col span="12">
-                            <cpt:fieldSet name="Identity" id="identity"/>
+                            <cpt:fieldSet name="Id" id="id">
+                                <cpt:action id="delete" position="PANEL"/>
+                                <cpt:property id="interactionId"/>
+                                <cpt:property id="timestamp"/>
+                            </cpt:fieldSet>
                         </bs:col>
                     </bs:row>
                 </bs:tab>
@@ -22,19 +28,29 @@
                 <bs:tab name="Metadata">
                     <bs:row>
                         <bs:col span="12">
-                            <cpt:fieldSet name="Metadata" id="metadata"/>
+                            <cpt:fieldSet name="Metadata" id="metadata">
+                                <cpt:property id="logicalTypeName"/>
+                                <cpt:property id="interactionId"/>
+                            </cpt:fieldSet>
                         </bs:col>
                     </bs:row>
                 </bs:tab>
             </bs:tabGroup>
-        </bs:col>
-    </bs:row>
-    <bs:row>
-        <bs:col span="6">
-            <cpt:fieldSet name="Details" id="details"/>
+            <cpt:fieldSet name="Target" id="target">
+                <cpt:property id="targetType"/>
+                <cpt:property id="targetId"/>
+                <cpt:property id="member"/>
+            </cpt:fieldSet>
+            <cpt:fieldSet name="Control" id="control">
+                <cpt:action id="replayOrRetry" position="PANEL"/>
+                <cpt:property id="replayState"/>
+            </cpt:fieldSet>
         </bs:col>
         <bs:col span="6">
-            <cpt:fieldSet name="Command Dto" id="dto"/>
+            <cpt:fieldSet name="Command Dto" id="dto">
+                <cpt:action id="openCommandLogEntry" position="PANEL"/>
+                <cpt:property id="dto"/>
+            </cpt:fieldSet>
         </bs:col>
     </bs:row>
     <bs:row>
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_delete.java
similarity index 50%
copy from extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java
copy to extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_delete.java
index fb65cb4a511..6289ab9647e 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_delete.java
@@ -18,17 +18,32 @@
  */
 package org.apache.causeway.extensions.commandlog.applib.dom.replay;
 
-import org.apache.causeway.applib.services.command.CommandExecutorService;
-import org.apache.causeway.applib.services.iactn.InteractionService;
-import org.apache.causeway.applib.services.repository.RepositoryService;
-import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
+import lombok.RequiredArgsConstructor;
 
-/**
- * Bundles dependencies for the replay logic.
- */
-public record ReplayContext(
-        RepositoryService repositoryService,
-        InteractionService interactionService,
-        CommandLogEntryRepository commandLogEntryRepository,
-        CommandExecutorService commandExecutorService) {
+import org.apache.causeway.applib.annotation.*;
+
+@Action(
+        restrictTo = RestrictTo.PROTOTYPING,
+        semantics = SemanticsOf.NON_IDEMPOTENT,
+        commandPublishing = Publishing.DISABLED,
+        domainEvent = ReplayableCommand_delete.DomainEvent.class,
+        executionPublishing = Publishing.DISABLED
+)
+@ActionLayout(
+        sequence = "0.3",
+        //hidden = Where.NOWHERE, // show in tables //TODO NPE bug
+        describedAs = "Deletes the associated Command Log Entry (cannot be undone)"
+)
+@RequiredArgsConstructor
+public class ReplayableCommand_delete {
+
+    public static class DomainEvent extends ReplayableCommand.ActionDomainEvent<ReplayableCommand_delete> {
+    }
+
+    private final ReplayableCommand replayableCommand;
+
+    @MemberSupport
+    public void act() {
+        replayableCommand.deleteObj();
+    }
 }
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_excludeFromReplay.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_excludeFromReplay.java
new file mode 100644
index 00000000000..955c7ef4c2d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_excludeFromReplay.java
@@ -0,0 +1,54 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.commandlog.applib.dom.replay;
+
+import lombok.RequiredArgsConstructor;
+
+import org.apache.causeway.applib.annotation.*;
+
+@Action(
+        restrictTo = RestrictTo.PROTOTYPING,
+        commandPublishing = Publishing.DISABLED,
+        domainEvent = ReplayableCommand_excludeFromReplay.DomainEvent.class,
+        executionPublishing = Publishing.DISABLED
+)
+@ActionLayout(
+        //hidden = Where.NOWHERE, // show in tables //TODO NPE bug
+        sequence = "2.2",
+        associateWith = "replayState",
+        describedAs = "Marks Command to be EXCLUDED from replay."
+)
+@RequiredArgsConstructor
+public class ReplayableCommand_excludeFromReplay {
+
+    public static class DomainEvent extends ReplayableCommand.ActionDomainEvent<ReplayableCommand_excludeFromReplay> {
+    }
+
+    private final ReplayableCommand replayableCommand;
+
+    @MemberSupport
+    public ReplayableCommand act() {
+        return replayableCommand.excludeFromReplay();
+    }
+
+    @MemberSupport
+    private String disableAct() {
+        return replayableCommand.disableExcludeFromReplay();
+    }
+}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_makeExportable.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_makeExportable.java
new file mode 100644
index 00000000000..591746fa0c9
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_makeExportable.java
@@ -0,0 +1,55 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.commandlog.applib.dom.replay;
+
+import lombok.RequiredArgsConstructor;
+
+import org.apache.causeway.applib.annotation.*;
+
+@Action(
+        restrictTo = RestrictTo.PROTOTYPING,
+        semantics = SemanticsOf.IDEMPOTENT,
+        commandPublishing = Publishing.DISABLED,
+        domainEvent = ReplayableCommand_makeExportable.DomainEvent.class,
+        executionPublishing = Publishing.DISABLED
+)
+@ActionLayout(
+        //hidden = Where.NOWHERE, // show in tables //TODO NPE bug
+        associateWith = "replayState",
+        sequence = "2.1",
+        describedAs = "Makes Command exportable (again)"
+)
+@RequiredArgsConstructor
+public class ReplayableCommand_makeExportable {
+
+    public static class DomainEvent extends ReplayableCommand.ActionDomainEvent<ReplayableCommand_makeExportable> {
+    }
+
+    private final ReplayableCommand replayableCommand;
+
+    @MemberSupport
+    public ReplayableCommand act() {
+        return replayableCommand.makeExportable();
+    }
+
+    @MemberSupport
+    public String disableAct() {
+        return replayableCommand.disableMakeExportable();
+    }
+}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_openCommandLogEntry.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_openCommandLogEntry.java
new file mode 100644
index 00000000000..600744eab9c
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_openCommandLogEntry.java
@@ -0,0 +1,54 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.commandlog.applib.dom.replay;
+
+import lombok.RequiredArgsConstructor;
+
+import org.apache.causeway.applib.annotation.*;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry;
+
+@Action(
+        semantics = SemanticsOf.SAFE,
+        commandPublishing = Publishing.DISABLED,
+        domainEvent = ReplayableCommand_openCommandLogEntry.DomainEvent.class,
+        executionPublishing = Publishing.DISABLED
+)
+@ActionLayout(
+        sequence = "0.2",
+        describedAs = "Opens the underlying Command Log Entry"
+)
+@RequiredArgsConstructor
+public class ReplayableCommand_openCommandLogEntry {
+
+    public static class DomainEvent extends ReplayableCommand.ActionDomainEvent<ReplayableCommand_openCommandLogEntry> {
+    }
+
+    private final ReplayableCommand replayableCommand;
+
+    @MemberSupport
+    public CommandLogEntry act() {
+        return replayableCommand.commandLogEntry()
+                .orElse(null);
+    }
+
+    @MemberSupport
+    public String disableAct() {
+        return replayableCommand.commandLogEntry().isEmpty() ? "No corresponding CommandLogEntry" : null;
+    }
+}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_replayOrRetry.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_replayOrRetry.java
new file mode 100644
index 00000000000..e2847773951
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_replayOrRetry.java
@@ -0,0 +1,57 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.commandlog.applib.dom.replay;
+
+import lombok.RequiredArgsConstructor;
+
+import org.apache.causeway.applib.annotation.*;
+
+@Action(
+        restrictTo = RestrictTo.PROTOTYPING,
+        semantics = SemanticsOf.NON_IDEMPOTENT,
+        commandPublishing = Publishing.DISABLED,
+        domainEvent = ReplayableCommand_replayOrRetry.DomainEvent.class,
+        executionPublishing = Publishing.DISABLED
+)
+@ActionLayout(
+        sequence = "0.1",
+        cssClassFa = "solid circle-play",
+        cssClass = "btn-primary"
+        //hidden = Where.NOWHERE // show in tables //TODO NPE bug
+)
+@RequiredArgsConstructor
+public class ReplayableCommand_replayOrRetry {
+
+    public static class DomainEvent extends ReplayableCommand.ActionDomainEvent<ReplayableCommand_replayOrRetry> {
+    }
+
+    private final ReplayableCommand replayableCommand;
+
+
+    @MemberSupport
+    public ReplayableCommand act() {
+        replayableCommand.tryReplayOrRetry();
+        return replayableCommand;
+    }
+
+    @MemberSupport
+    public String disableAct() {
+        return replayableCommand.disableReplayOrRetry();
+    }
+}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/TimestampMarshallUtil.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/TimestampMarshallUtil.java
new file mode 100644
index 00000000000..ba8eaab85a4
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/TimestampMarshallUtil.java
@@ -0,0 +1,48 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.commandlog.applib.dom.replay;
+
+import lombok.experimental.UtilityClass;
+
+import java.sql.Timestamp;
+import java.time.Instant;
+
+@UtilityClass
+class TimestampMarshallUtil {
+    private static final java.time.format.DateTimeFormatter VM_MEMENTO_FORMATTER =
+            java.time.format.DateTimeFormatter
+                    .ofPattern("uuuu-MM-dd'T'HH-mm-ss.SSSX")
+                    .withZone(java.time.ZoneOffset.UTC);
+
+    static String toString(Timestamp ts) {
+        // Human-readable and URL-friendly (no ':' or '~').
+        return VM_MEMENTO_FORMATTER.format(ts.toInstant()); // e.g. 2026-04-22T02-00-00.000Z
+    }
+
+    static Timestamp fromString(String s, Timestamp fallback) {
+        if (s == null || s.isBlank()) {
+            return fallback;
+        }
+        try {
+            return Timestamp.from(Instant.from(VM_MEMENTO_FORMATTER.parse(s)));
+        } catch (Exception e) {
+            return fallback;
+        }
+    }
+}
diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
index 1c94cd85468..8c280960613 100644
--- a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
@@ -359,9 +359,10 @@ void test_all_the_repository_methods() {
         var username1 = commandTarget1User1.getUsername();
         var from = commandTarget1User1.getStartedAt().toLocalDateTime().toLocalDate();
         var to = from.plusDays(1);
+        final var timestamp = commandTarget1User1.getTimestamp();
 
         // when
-        List<? extends CommandLogEntry> notYetReplayed = commandLogEntryRepository.findReplayPendingOrFailed();
+        List<? extends CommandLogEntry> notYetReplayed = commandLogEntryRepository.findForegroundSinceTimestampAndWithReplayPendingOrFailed(timestamp);
 
         // then
         Assertions.assertThat(notYetReplayed).isEmpty();
@@ -374,7 +375,7 @@ void test_all_the_repository_methods() {
             commandTarget1User1.setReplayState(ReplayState.PENDING);
 
             // when
-            List<? extends CommandLogEntry> notYetReplayed2 = commandLogEntryRepository.findReplayPendingOrFailed();
+            List<? extends CommandLogEntry> notYetReplayed2 = commandLogEntryRepository.findForegroundSinceTimestampAndWithReplayPendingOrFailed(timestamp);
 
             // then
             Assertions.assertThat(notYetReplayed2).hasSize(1);
diff --git a/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java b/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java
index 0ac083a3198..65c156e70e0 100644
--- a/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java
+++ b/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java
@@ -209,10 +209,20 @@
                   + "   AND cl.completedAt is not null "
                   + " ORDER BY cl.timestamp DESC"), // programmatic LIMIT 1
     @NamedQuery(
-            name  = Nq.FIND_BY_REPLAY_STATE,
+            name  = Nq.FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATE,
             query = "SELECT cl "
                   + " FROM CommandLogEntry cl "
-                  + " WHERE (cl.replayState = :replayState1 OR cl.replayState = :replayState2) "
+                  + " WHERE cl.executeIn = org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn.FOREGROUND "
+                  + "   AND cl.timestamp >= :from "
+                  + "   AND cl.replayState = :replayState "
+                  + " ORDER BY cl.timestamp ASC"),
+    @NamedQuery(
+            name  = Nq.FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATES,
+            query = "SELECT cl "
+                  + " FROM CommandLogEntry cl "
+                  + " WHERE cl.executeIn = org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn.FOREGROUND "
+                  + "   AND cl.timestamp >= :from "
+                  + "   AND (cl.replayState = :replayState1 OR cl.replayState = :replayState2) "
                   + " ORDER BY cl.timestamp ASC"),
 })
 @Named(CommandLogEntry.LOGICAL_TYPE_NAME)