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

(causeway) 04/22: CAUSEWAY-3910: reconcile command recording core policy

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

danhaywood pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 2ddd9bc37fdaecf35b85c08bdbc12de27e4871e9
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Tue Aug 4 10:57:57 2026 +0100

    CAUSEWAY-3910: reconcile command recording core policy
---
 .../command/CommandRecordingSuppressed.java        |  30 +++++
 .../services/command/PauseCommandLoggingEvent.java |  37 ++++++
 .../command/ResumeCommandLoggingEvent.java         |  37 ++++++
 .../services/command/CommandLoggingEventTest.java  |  34 ++++++
 .../config/pages/sections/causeway.extensions.adoc |   9 ++
 .../core/config/CausewayConfiguration.java         |  26 +++++
 ...figuration_CommandLogRecordingSupport_Test.java |  45 +++++++
 .../executor/MemberExecutorServiceDefault.java     |  11 ++
 .../executor/MemberExecutorServiceDefaultTest.java | 130 +++++++++++++++++++++
 .../applib/CausewayModuleExtCommandLogApplib.java  |   4 +
 .../commandlog/applib/dom/CommandLogEntry.java     |   3 +-
 .../applib/dom/replay/CommandExportManager.java    |   3 +-
 .../applib/dom/replay/CommandReplayManager.java    |   3 +-
 .../applib/dom/replay/ReplayableCommand.java       |   4 +-
 .../applib/subscriber/CommandLogPauseState.java    |  53 +++++++++
 .../subscriber/CommandLogPauseStateListener.java   |  60 ++++++++++
 .../subscriber/CommandSubscriberForCommandLog.java |   7 +-
 .../replay/CommandRecordingSuppressionTest.java    |  41 +++++++
 .../subscriber/CommandLogPauseStateTest.java       |  56 +++++++++
 .../CommandSubscriberForCommandLogTest.java        | 100 ++++++++++++++++
 .../tasks.md                                       |  24 ++--
 .../services/InitialFixtureScriptsInstaller.java   |  15 ++-
 .../InitialFixtureScriptsInstallerTest.java        |  87 ++++++++++++++
 23 files changed, 798 insertions(+), 21 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/command/CommandRecordingSuppressed.java b/api/applib/src/main/java/org/apache/causeway/applib/services/command/CommandRecordingSuppressed.java
new file mode 100644
index 00000000000..be11e9b2745
--- /dev/null
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/command/CommandRecordingSuppressed.java
@@ -0,0 +1,30 @@
+/*
+ *  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.applib.services.command;
+
+/**
+ * Marker interface for objects whose member interactions are ignored by command recording support.
+ *
+ * <p>Implementing this interface opts the target object out of command recording.
+ * The member invocation itself still executes normally; only command recording is suppressed.
+ *
+ * @since 4.0 {@index}
+ */
+public interface CommandRecordingSuppressed {
+}
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/command/PauseCommandLoggingEvent.java b/api/applib/src/main/java/org/apache/causeway/applib/services/command/PauseCommandLoggingEvent.java
new file mode 100644
index 00000000000..5b3321563cb
--- /dev/null
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/command/PauseCommandLoggingEvent.java
@@ -0,0 +1,37 @@
+/*
+ *  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.applib.services.command;
+
+import org.apache.causeway.applib.events.EventObjectBase;
+
+/**
+ * Pauses command-log persistence for a scoped operation.
+ *
+ * <p>Every pause event should be paired with a {@link ResumeCommandLoggingEvent}, preferably from a
+ * {@code finally} block.
+ * Command logging implementations may support nested pauses by tracking pause depth.
+ *
+ * @since 4.0 {@index}
+ */
+public class PauseCommandLoggingEvent extends EventObjectBase<Object> {
+
+    public PauseCommandLoggingEvent(final Object source) {
+        super(source);
+    }
+}
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/command/ResumeCommandLoggingEvent.java b/api/applib/src/main/java/org/apache/causeway/applib/services/command/ResumeCommandLoggingEvent.java
new file mode 100644
index 00000000000..bf6e2666cfe
--- /dev/null
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/command/ResumeCommandLoggingEvent.java
@@ -0,0 +1,37 @@
+/*
+ *  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.applib.services.command;
+
+import org.apache.causeway.applib.events.EventObjectBase;
+
+/**
+ * Resumes command-log persistence after a scoped pause.
+ *
+ * <p>One resume event should be posted for each matching {@link PauseCommandLoggingEvent}.
+ * Command logging implementations should tolerate unmatched resume events without allowing pause depth to become
+ * negative.
+ *
+ * @since 4.0 {@index}
+ */
+public class ResumeCommandLoggingEvent extends EventObjectBase<Object> {
+
+    public ResumeCommandLoggingEvent(final Object source) {
+        super(source);
+    }
+}
diff --git a/api/applib/src/test/java/org/apache/causeway/applib/services/command/CommandLoggingEventTest.java b/api/applib/src/test/java/org/apache/causeway/applib/services/command/CommandLoggingEventTest.java
new file mode 100644
index 00000000000..dba054cb396
--- /dev/null
+++ b/api/applib/src/test/java/org/apache/causeway/applib/services/command/CommandLoggingEventTest.java
@@ -0,0 +1,34 @@
+/*
+ *  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.applib.services.command;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CommandLoggingEventTest {
+
+    @Test
+    void eventsRetainTheirSource() {
+        var source = new Object();
+
+        assertThat(new PauseCommandLoggingEvent(source).getSource()).isSameAs(source);
+        assertThat(new ResumeCommandLoggingEvent(source).getSource()).isSameAs(source);
+    }
+}
diff --git a/core/config/src/main/adoc/modules/config/pages/sections/causeway.extensions.adoc b/core/config/src/main/adoc/modules/config/pages/sections/causeway.extensions.adoc
index 18a0d81c8e5..b982956da51 100644
--- a/core/config/src/main/adoc/modules/config/pages/sections/causeway.extensions.adoc
+++ b/core/config/src/main/adoc/modules/config/pages/sections/causeway.extensions.adoc
@@ -29,6 +29,15 @@ persist
 | null
 
 
+|
+[[causeway.extensions.command-log.recording-support]]
+causeway.extensions.command-log. +
+recording-support
+
+|  DISABLED
+| Whether command recording support is enabled.
+
+
 |
 [[causeway.extensions.command-log.run-background-commands.batch-size]]
 causeway.extensions.command-log. +
diff --git a/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java b/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
index 9478fb4155c..545f893e606 100644
--- a/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
+++ b/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
@@ -3651,6 +3651,15 @@ public record CommandLog(
              */
             @DefaultValue("ENABLED")
             PersistPolicy persist,
+            /**
+             * Whether command recording support is enabled.
+             *
+             * <p>When enabled, recording-aware policies may broaden command publishing eligibility while
+             * honoring {@code CommandRecordingSuppressed}.  Disabled by default to preserve existing
+             * command-publishing behavior.
+             */
+            @DefaultValue("DISABLED")
+            RecordingSupport recordingSupport,
             @DefaultValue
             RunBackgroundCommands runBackgroundCommands) {
 
@@ -3674,6 +3683,23 @@ public enum PersistPolicy {
                 public boolean isDisabled() { return this == DISABLED; }
             }
 
+            /**
+             * As per {@link CommandLog#recordingSupport()}.
+             */
+            public enum RecordingSupport {
+                /**
+                 * Enable command recording support.
+                 */
+                ENABLED,
+                /**
+                 * Disable command recording support.  This is the default.
+                 */
+                DISABLED;
+
+                public boolean isEnabled() { return this == ENABLED; }
+                public boolean isDisabled() { return this == DISABLED; }
+            }
+
             public record RunBackgroundCommands(
                 /**
                  * Limits the number of pending commands that the <code>RunBackgroundCommandsJob</code>
diff --git a/core/config/src/test/java/org/apache/causeway/core/config/CausewayConfiguration_CommandLogRecordingSupport_Test.java b/core/config/src/test/java/org/apache/causeway/core/config/CausewayConfiguration_CommandLogRecordingSupport_Test.java
new file mode 100644
index 00000000000..661723ba6ad
--- /dev/null
+++ b/core/config/src/test/java/org/apache/causeway/core/config/CausewayConfiguration_CommandLogRecordingSupport_Test.java
@@ -0,0 +1,45 @@
+/*
+ *  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.core.config;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.test.util.TestPropertyValues;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CausewayConfiguration_CommandLogRecordingSupport_Test {
+
+    private final ConfigurationFactory configurationFactory = new ConfigurationFactory();
+
+    @Test
+    void disabledByDefault() {
+        configurationFactory.test(
+            TestPropertyValues.empty(),
+            causeway -> assertThat(causeway.extensions().commandLog().recordingSupport())
+                .isEqualTo(CausewayConfiguration.Extensions.CommandLog.RecordingSupport.DISABLED));
+    }
+
+    @Test
+    void canBeEnabled() {
+        configurationFactory.test(
+            TestPropertyValues.of("causeway.extensions.command-log.recording-support=ENABLED"),
+            causeway -> assertThat(causeway.extensions().commandLog().recordingSupport().isEnabled()).isTrue());
+    }
+}
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java
index 8be99d381c8..e9a91cb2492 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java
@@ -32,6 +32,7 @@
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.services.command.Command;
+import org.apache.causeway.applib.services.command.CommandRecordingSuppressed;
 import org.apache.causeway.applib.services.iactn.ActionInvocation;
 import org.apache.causeway.applib.services.iactn.Execution;
 import org.apache.causeway.applib.services.iactn.Interaction;
@@ -409,6 +410,9 @@ void prepareCommandForPublishing(
             final @NonNull ObjectMember objectMember,
             final @NonNull FacetHolder facetHolder) {
 
+        if(targetSuppressesCommandRecording(interactionHead))
+            return;
+
         if(interactionHead.isCommandForMember(command, objectMember)
                 && isPublishingEnabled(facetHolder)) {
             command.updater().setPublishingPhase(Command.CommandPublishingPhase.READY);
@@ -417,6 +421,13 @@ && isPublishingEnabled(facetHolder)) {
         commandPublisher().ready(command);
     }
 
+    private boolean targetSuppressesCommandRecording(final InteractionHead interactionHead) {
+        var ownerPojo = MmUnwrapUtils.single(interactionHead.owner());
+        var targetPojo = MmUnwrapUtils.single(interactionHead.target());
+        return ownerPojo instanceof CommandRecordingSuppressed
+                || targetPojo instanceof CommandRecordingSuppressed;
+    }
+
     private CommandPublisher commandPublisher() {
     	return commandPublisherProvider.get();
     }
diff --git a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefaultTest.java b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefaultTest.java
new file mode 100644
index 00000000000..ac429dff3f4
--- /dev/null
+++ b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefaultTest.java
@@ -0,0 +1,130 @@
+/*
+ *  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.core.runtimeservices.executor;
+
+import java.util.Optional;
+
+import jakarta.inject.Provider;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.causeway.applib.services.command.Command;
+import org.apache.causeway.applib.services.command.CommandRecordingSuppressed;
+import org.apache.causeway.applib.services.metamodel.BeanSort;
+import org.apache.causeway.core.config.observation.CausewayObservationIntegration;
+import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
+import org.apache.causeway.core.metamodel.interactions.InteractionHead;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.core.metamodel.services.publishing.CommandPublisher;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
+import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+class MemberExecutorServiceDefaultTest {
+
+    @Test
+    void suppressedTargetBypassesCommandPreparation() {
+        var commandPublisherProvider = commandPublisherProvider();
+        var service = newService(commandPublisherProvider);
+        var interactionHead = interactionHead(new Object(), new SuppressedTarget());
+        var command = mock(Command.class);
+        var objectMember = mock(ObjectMember.class);
+        var facetHolder = mock(FacetHolder.class);
+
+        service.prepareCommandForPublishing(command, interactionHead, objectMember, facetHolder);
+
+        verifyNoInteractions(commandPublisherProvider, command, objectMember, facetHolder);
+    }
+
+    @Test
+    void suppressedOwnerBypassesCommandPreparation() {
+        var commandPublisherProvider = commandPublisherProvider();
+        var service = newService(commandPublisherProvider);
+        var interactionHead = interactionHead(new SuppressedTarget(), new Object());
+        var command = mock(Command.class);
+        var objectMember = mock(ObjectMember.class);
+        var facetHolder = mock(FacetHolder.class);
+
+        service.prepareCommandForPublishing(command, interactionHead, objectMember, facetHolder);
+
+        verifyNoInteractions(commandPublisherProvider, command, objectMember, facetHolder);
+    }
+
+    @Test
+    void unmarkedTargetRetainsNormalPublisherNotification() {
+        var commandPublisher = mock(CommandPublisher.class);
+        var commandPublisherProvider = commandPublisherProvider();
+        when(commandPublisherProvider.get()).thenReturn(commandPublisher);
+        var service = newService(commandPublisherProvider);
+        var interactionHead = interactionHead(new Object(), new Object());
+        var command = mock(Command.class);
+
+        service.prepareCommandForPublishing(
+            command,
+            interactionHead,
+            mock(ObjectMember.class),
+            mock(FacetHolder.class));
+
+        verify(commandPublisher).ready(command);
+    }
+
+    private InteractionHead interactionHead(final Object ownerPojo, final Object targetPojo) {
+        var owner = managedObject(ownerPojo);
+        var target = managedObject(targetPojo);
+        var interactionHead = mock(InteractionHead.class);
+        when(interactionHead.owner()).thenReturn(owner);
+        when(interactionHead.target()).thenReturn(target);
+        return interactionHead;
+    }
+
+    private ManagedObject managedObject(final Object pojo) {
+        var objectSpecification = mock(ObjectSpecification.class);
+        var specificationLoader = mock(SpecificationLoader.class);
+        when(objectSpecification.isValue()).thenReturn(true);
+        when(objectSpecification.getBeanSort()).thenReturn(BeanSort.VALUE);
+        when(objectSpecification.getSpecificationLoader()).thenReturn(specificationLoader);
+        when(specificationLoader.specForType(pojo.getClass())).thenReturn(Optional.of(objectSpecification));
+        return ManagedObject.value(objectSpecification, pojo);
+    }
+
+    @SuppressWarnings("unchecked")
+    private Provider<CommandPublisher> commandPublisherProvider() {
+        return mock(Provider.class);
+    }
+
+    private MemberExecutorServiceDefault newService(final Provider<CommandPublisher> commandPublisherProvider) {
+        return new MemberExecutorServiceDefault(
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            commandPublisherProvider,
+            mock(CausewayObservationIntegration.class));
+    }
+
+    private static final class SuppressedTarget implements CommandRecordingSuppressed {
+    }
+}
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 169370b4001..e2f451f1cdb 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
@@ -52,6 +52,8 @@
 import org.apache.causeway.extensions.commandlog.applib.job.BackgroundCommandsJobControl;
 import org.apache.causeway.extensions.commandlog.applib.job.RunBackgroundCommandsJob;
 import org.apache.causeway.extensions.commandlog.applib.spi.RunBackgroundCommandsJobListener;
+import org.apache.causeway.extensions.commandlog.applib.subscriber.CommandLogPauseState;
+import org.apache.causeway.extensions.commandlog.applib.subscriber.CommandLogPauseStateListener;
 import org.apache.causeway.extensions.commandlog.applib.subscriber.CommandSubscriberForCommandLog;
 
 @Configuration(proxyBeanMethods = false)
@@ -95,6 +97,8 @@
         RunBackgroundCommandsJobListener.Noop.class,
 
         // @Service's
+        CommandLogPauseState.class,
+        CommandLogPauseStateListener.class,
         CommandSubscriberForCommandLog.class,
         CommandLogEntry.TableColumnOrderDefault.class,
 
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 99a167025f0..0fff50c3b11 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
@@ -51,6 +51,7 @@
 import org.apache.causeway.applib.annotation.Publishing;
 import org.apache.causeway.applib.annotation.Where;
 import org.apache.causeway.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
+import org.apache.causeway.applib.services.command.CommandRecordingSuppressed;
 import org.apache.causeway.applib.mixins.system.DomainChangeRecord;
 import org.apache.causeway.applib.mixins.system.HasInteractionId;
 import org.apache.causeway.applib.services.bookmark.Bookmark;
@@ -96,7 +97,7 @@
         cssClassUiEvent = CommandLogEntry.CssClassUiEvent.class,
         layoutUiEvent = CommandLogEntry.LayoutUiEvent.class)
 public interface CommandLogEntry
-extends Comparable<CommandLogEntry>, DomainChangeRecord, HasCommandDto {
+extends Comparable<CommandLogEntry>, DomainChangeRecord, HasCommandDto, CommandRecordingSuppressed {
 
     public final static String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandLogEntry";
     public static final String SCHEMA = CausewayModuleExtCommandLogApplib.SCHEMA;
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 275aee2c82a..bfe33fc6c0b 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
@@ -45,6 +45,7 @@
 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.services.command.CommandRecordingSuppressed;
 import org.apache.causeway.applib.value.Clob;
 import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
 import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib;
@@ -61,7 +62,7 @@ public record CommandExportManager(
         @Property
         @PropertyLayout(describedAs = "Only commands after this timestamp are available for export")
         java.sql.Timestamp baseline,
-        ReplayContext replayContext) implements ViewModel {
+        ReplayContext replayContext) implements ViewModel, CommandRecordingSuppressed {
 
 	public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandExportManager";
 
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 4dae27d07cb..d48d19a0522 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
@@ -45,6 +45,7 @@
 import org.apache.causeway.applib.annotation.RestrictTo;
 import org.apache.causeway.applib.annotation.SemanticsOf;
 import org.apache.causeway.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
+import org.apache.causeway.applib.services.command.CommandRecordingSuppressed;
 import org.apache.causeway.applib.util.schema.CommandDtoUtils;
 import org.apache.causeway.applib.value.Blob;
 import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib;
@@ -60,7 +61,7 @@ public record CommandReplayManager(
         @Property
         @PropertyLayout(describedAs = "Only commands after this baseline are listed for replay")
         java.sql.Timestamp baseline,
-        ReplayContext replayContext) implements ViewModel {
+        ReplayContext replayContext) implements ViewModel, CommandRecordingSuppressed {
 
     public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandReplayManager";
 
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 82e249c5948..8a7492fdacf 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
@@ -48,6 +48,7 @@
 import org.apache.causeway.applib.jaxb.JavaTimeXMLGregorianCalendarMarshalling;
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.applib.services.bookmark.BookmarkService;
+import org.apache.causeway.applib.services.command.CommandRecordingSuppressed;
 import org.apache.causeway.applib.services.command.CommandExecutorService.InteractionContextPolicy;
 import org.apache.causeway.commons.functional.Try;
 import org.apache.causeway.commons.internal.base._Refs.ObjectReference;
@@ -76,7 +77,8 @@
 @DomainObjectLayout//(cssClassFa = "terminal")
 @Named(ReplayableCommand.LOGICAL_TYPE_NAME)
 @AllArgsConstructor
-public final class ReplayableCommand implements ViewModel, Comparable<ReplayableCommand> {
+public final class ReplayableCommand
+implements ViewModel, Comparable<ReplayableCommand>, CommandRecordingSuppressed {
 
     public static abstract class ActionDomainEvent<T>
             extends CausewayModuleExtCommandLogApplib.ActionDomainEvent<T> { }
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseState.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseState.java
new file mode 100644
index 00000000000..ce18d0e1fec
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseState.java
@@ -0,0 +1,53 @@
+/*
+ *  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.subscriber;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jakarta.inject.Named;
+
+import org.springframework.stereotype.Service;
+
+import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib;
+
+/**
+ * Tracks application-wide, nested command-log persistence pauses.
+ *
+ * @since 4.0 {@index}
+ */
+@Service
+@Named(CommandLogPauseState.LOGICAL_TYPE_NAME)
+public class CommandLogPauseState {
+
+    static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandLogPauseState";
+
+    private final AtomicInteger pauseDepth = new AtomicInteger();
+
+    public boolean isPaused() {
+        return pauseDepth.get() > 0;
+    }
+
+    public void pause() {
+        pauseDepth.incrementAndGet();
+    }
+
+    public void resume() {
+        pauseDepth.updateAndGet(current -> current > 0 ? current - 1 : 0);
+    }
+}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseStateListener.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseStateListener.java
new file mode 100644
index 00000000000..689be353859
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseStateListener.java
@@ -0,0 +1,60 @@
+/*
+ *  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.subscriber;
+
+import jakarta.annotation.Priority;
+import jakarta.inject.Inject;
+import jakarta.inject.Named;
+
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Service;
+
+import org.apache.causeway.applib.annotation.PriorityPrecedence;
+import org.apache.causeway.applib.services.command.PauseCommandLoggingEvent;
+import org.apache.causeway.applib.services.command.ResumeCommandLoggingEvent;
+import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib;
+
+import lombok.RequiredArgsConstructor;
+
+/**
+ * Maintains command-log pause state in response to application events.
+ *
+ * @since 4.0 {@index}
+ */
+@Service
+@Named(CommandLogPauseStateListener.LOGICAL_TYPE_NAME)
+@Priority(PriorityPrecedence.MIDPOINT)
+@RequiredArgsConstructor(onConstructor_ = {@Inject})
+public class CommandLogPauseStateListener {
+
+    static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE
+            + ".CommandLogPauseStateListener";
+
+    private final CommandLogPauseState commandLogPauseState;
+
+    @EventListener(PauseCommandLoggingEvent.class)
+    public void onPauseCommandLoggingEvent(final PauseCommandLoggingEvent event) {
+        commandLogPauseState.pause();
+    }
+
+    @EventListener(ResumeCommandLoggingEvent.class)
+    public void onResumeCommandLoggingEvent(final ResumeCommandLoggingEvent event) {
+        commandLogPauseState.resume();
+    }
+}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java
index 17f83fd2ac2..4cec276dd52 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java
@@ -61,6 +61,7 @@ public class CommandSubscriberForCommandLog implements CommandSubscriber {
     final RepositoryService repositoryService;
     final CausewayConfiguration causewayConfiguration;
     final ClockService clockService;
+    final CommandLogPauseState commandLogPauseState;
 
     @Override
     public boolean isEnabled() {
@@ -70,7 +71,7 @@ public boolean isEnabled() {
     @Override
     public void onReady(final Command command) {
 
-        if (!isEnabled())
+        if (!isEnabled() || commandLogPauseState.isPaused())
             return;
 
         var existingCommandLogEntryIfAny =
@@ -112,7 +113,7 @@ public void onReady(final Command command) {
     @Override
     public void onStarted(final Command command) {
 
-        if (!isEnabled())
+        if (!isEnabled() || commandLogPauseState.isPaused())
             return;
 
         commandLogEntryRepository.findByInteractionId(command.getInteractionId())
@@ -124,7 +125,7 @@ public void onStarted(final Command command) {
     @Override
     public void onCompleted(final Command command) {
 
-        if (!isEnabled())
+        if (!isEnabled() || commandLogPauseState.isPaused())
             return;
 
         commandLogEntryRepository.findByInteractionId(command.getInteractionId())
diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandRecordingSuppressionTest.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandRecordingSuppressionTest.java
new file mode 100644
index 00000000000..6d526e2c54c
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandRecordingSuppressionTest.java
@@ -0,0 +1,41 @@
+/*
+ *  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 org.junit.jupiter.api.Test;
+
+import org.apache.causeway.applib.services.command.CommandRecordingSuppressed;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CommandRecordingSuppressionTest {
+
+    @Test
+    void commandlogHelperTypesOptOutOfRecording() {
+        assertThat(isSuppressed(CommandExportManager.class)).isTrue();
+        assertThat(isSuppressed(CommandReplayManager.class)).isTrue();
+        assertThat(isSuppressed(ReplayableCommand.class)).isTrue();
+        assertThat(isSuppressed(CommandLogEntry.class)).isTrue();
+    }
+
+    private boolean isSuppressed(final Class<?> type) {
+        return CommandRecordingSuppressed.class.isAssignableFrom(type);
+    }
+}
diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseStateTest.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseStateTest.java
new file mode 100644
index 00000000000..4207881d427
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseStateTest.java
@@ -0,0 +1,56 @@
+/*
+ *  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.subscriber;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.causeway.applib.services.command.PauseCommandLoggingEvent;
+import org.apache.causeway.applib.services.command.ResumeCommandLoggingEvent;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CommandLogPauseStateTest {
+
+    private final CommandLogPauseState pauseState = new CommandLogPauseState();
+    private final CommandLogPauseStateListener listener = new CommandLogPauseStateListener(pauseState);
+
+    @Test
+    void nestedPausesRequireMatchingResumes() {
+        listener.onPauseCommandLoggingEvent(new PauseCommandLoggingEvent(this));
+        listener.onPauseCommandLoggingEvent(new PauseCommandLoggingEvent(this));
+
+        listener.onResumeCommandLoggingEvent(new ResumeCommandLoggingEvent(this));
+        assertThat(pauseState.isPaused()).isTrue();
+
+        listener.onResumeCommandLoggingEvent(new ResumeCommandLoggingEvent(this));
+        assertThat(pauseState.isPaused()).isFalse();
+    }
+
+    @Test
+    void unmatchedResumeDoesNotMakeStatePausedOrNegative() {
+        listener.onResumeCommandLoggingEvent(new ResumeCommandLoggingEvent(this));
+        listener.onResumeCommandLoggingEvent(new ResumeCommandLoggingEvent(this));
+
+        assertThat(pauseState.isPaused()).isFalse();
+
+        listener.onPauseCommandLoggingEvent(new PauseCommandLoggingEvent(this));
+        listener.onResumeCommandLoggingEvent(new ResumeCommandLoggingEvent(this));
+        assertThat(pauseState.isPaused()).isFalse();
+    }
+}
diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLogTest.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLogTest.java
new file mode 100644
index 00000000000..08f0f314223
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLogTest.java
@@ -0,0 +1,100 @@
+/*
+ *  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.subscriber;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.causeway.applib.services.command.Command;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
+import org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class CommandSubscriberForCommandLogTest {
+
+    @Test
+    void pausedSubscriberSkipsReadyStartedAndCompletedNotifications() {
+        var command = new Command(UUID.randomUUID());
+        var commandLogEntryRepository = mock(CommandLogEntryRepository.class);
+        var commandLogPauseState = new CommandLogPauseState();
+        commandLogPauseState.pause();
+        var subscriber = subscriber(commandLogEntryRepository, commandLogPauseState);
+
+        subscriber.onReady(command);
+        subscriber.onStarted(command);
+        subscriber.onCompleted(command);
+
+        verify(commandLogEntryRepository, never()).findByInteractionId(command.getInteractionId());
+    }
+
+    @Test
+    void resumedSubscriberCreatesEntryForSubsequentReadyCommand() {
+        var command = new Command(UUID.randomUUID());
+        var commandLogEntryRepository = mock(CommandLogEntryRepository.class);
+        when(commandLogEntryRepository.findByInteractionId(command.getInteractionId())).thenReturn(Optional.empty());
+        var commandLogPauseState = new CommandLogPauseState();
+        commandLogPauseState.pause();
+        commandLogPauseState.resume();
+        var subscriber = subscriber(commandLogEntryRepository, commandLogPauseState);
+
+        subscriber.onReady(command);
+
+        verify(commandLogEntryRepository)
+            .createEntryAndPersist(command, command.getParentInteractionId(), ExecuteIn.FOREGROUND);
+    }
+
+    @Test
+    void unpausedSubscriberSynchronizesStartedAndCompletedNotifications() {
+        var command = new Command(UUID.randomUUID());
+        var commandLogEntry = mock(CommandLogEntry.class);
+        var commandLogEntryRepository = mock(CommandLogEntryRepository.class);
+        when(commandLogEntryRepository.findByInteractionId(command.getInteractionId()))
+            .thenReturn(Optional.of(commandLogEntry));
+        var subscriber = subscriber(commandLogEntryRepository, new CommandLogPauseState());
+
+        subscriber.onStarted(command);
+        subscriber.onCompleted(command);
+
+        verify(commandLogEntry, times(2)).sync(command);
+    }
+
+    private CommandSubscriberForCommandLog subscriber(
+            final CommandLogEntryRepository commandLogEntryRepository,
+            final CommandLogPauseState commandLogPauseState) {
+        return new CommandSubscriberForCommandLog(
+            commandLogEntryRepository,
+            null,
+            null,
+            null,
+            commandLogPauseState) {
+            @Override
+            public boolean isEnabled() {
+                return true;
+            }
+        };
+    }
+}
diff --git a/openspec/changes/reconcile-command-recording-core-policy/tasks.md b/openspec/changes/reconcile-command-recording-core-policy/tasks.md
index b7bbcf3c9a8..70681670026 100644
--- a/openspec/changes/reconcile-command-recording-core-policy/tasks.md
+++ b/openspec/changes/reconcile-command-recording-core-policy/tasks.md
@@ -1,29 +1,29 @@
 ## 1. Public contracts and configuration
 
-- [ ] 1.1 Add the Causeway 4 record-based `causeway.extensions.command-log.recording-support` configuration with `ENABLED` and `DISABLED` values, a disabled default, configuration documentation, and focused configuration tests.
-- [ ] 1.2 Add the core applib `CommandRecordingSuppressed`, `PauseCommandLoggingEvent`, and `ResumeCommandLoggingEvent` public contracts with API documentation and focused contract tests where applicable.
+- [x] 1.1 Add the Causeway 4 record-based `causeway.extensions.command-log.recording-support` configuration with `ENABLED` and `DISABLED` values, a disabled default, configuration documentation, and focused configuration tests.
+- [x] 1.2 Add the core applib `CommandRecordingSuppressed`, `PauseCommandLoggingEvent`, and `ResumeCommandLoggingEvent` public contracts with API documentation and focused contract tests where applicable.
 
 ## 2. Member execution suppression
 
-- [ ] 2.1 Adapt `MemberExecutorServiceDefault` so marked interaction owners or targets bypass command preparation while member execution continues normally.
-- [ ] 2.2 Add runtime-service tests covering marked owner, marked target, unmarked target, and unaffected member invocation behaviour.
+- [x] 2.1 Adapt `MemberExecutorServiceDefault` so marked interaction owners or targets bypass command preparation while member execution continues normally.
+- [x] 2.2 Add runtime-service tests covering marked owner, marked target, unmarked target, and unaffected member invocation behaviour.
 
 ## 3. Commandlog pause state
 
-- [ ] 3.1 Add the application-wide atomic pause-depth service and application-event listener, with unit tests for nesting and unmatched resume events.
-- [ ] 3.2 Make commandlog ready, started, and completed subscriber callbacks honour pause state, with focused subscriber or integration tests for creation and synchronization suppression.
+- [x] 3.1 Add the application-wide atomic pause-depth service and application-event listener, with unit tests for nesting and unmatched resume events.
+- [x] 3.2 Make commandlog ready, started, and completed subscriber callbacks honour pause state, with focused subscriber or integration tests for creation and synchronization suppression.
 
 ## 4. Framework-owned pause scope
 
-- [ ] 4.1 Publish paired pause and resume events around initial fixture-script installation using `try/finally` and without adding a commandlog extension dependency.
-- [ ] 4.2 Add fixture-installer tests covering successful execution, failed execution, event ordering, and guaranteed resume publication.
+- [x] 4.1 Publish paired pause and resume events around initial fixture-script installation using `try/finally` and without adding a commandlog extension dependency.
+- [x] 4.2 Add fixture-installer tests covering successful execution, failed execution, event ordering, and guaranteed resume publication.
 
 ## 5. Commandlog helper suppression
 
-- [ ] 5.1 Mark the existing export manager, replay manager, replayable command, and applicable command log entry abstractions as recording-suppressed.
-- [ ] 5.2 Add or adapt commandlog tests proving helper actions are not persisted through recording support while ordinary eligible interactions remain unaffected.
+- [x] 5.1 Mark the existing export manager, replay manager, replayable command, and applicable command log entry abstractions as recording-suppressed.
+- [x] 5.2 Add or adapt commandlog tests proving helper actions are not persisted through recording support while ordinary eligible interactions remain unaffected.
 
 ## 6. Verification
 
-- [ ] 6.1 Run the focused applib, configuration, runtime-services, testing-fixtures, and commandlog test suites and resolve regressions caused by the new contracts.
-- [ ] 6.2 Verify the external configuration property, Jakarta imports, public API documentation, and module dependency direction against the specification and programme ledger.
+- [x] 6.1 Run the focused applib, configuration, runtime-services, testing-fixtures, and commandlog test suites and resolve regressions caused by the new contracts.
+- [x] 6.2 Verify the external configuration property, Jakarta imports, public API documentation, and module dependency direction against the specification and programme ledger.
diff --git a/testing/fixtures/applib/src/main/java/org/apache/causeway/testing/fixtures/applib/services/InitialFixtureScriptsInstaller.java b/testing/fixtures/applib/src/main/java/org/apache/causeway/testing/fixtures/applib/services/InitialFixtureScriptsInstaller.java
index 0da1e4be909..262c42b6896 100644
--- a/testing/fixtures/applib/src/main/java/org/apache/causeway/testing/fixtures/applib/services/InitialFixtureScriptsInstaller.java
+++ b/testing/fixtures/applib/src/main/java/org/apache/causeway/testing/fixtures/applib/services/InitialFixtureScriptsInstaller.java
@@ -31,6 +31,9 @@
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.events.metamodel.MetamodelEvent;
+import org.apache.causeway.applib.services.command.PauseCommandLoggingEvent;
+import org.apache.causeway.applib.services.command.ResumeCommandLoggingEvent;
+import org.apache.causeway.applib.services.eventbus.EventBusService;
 import org.apache.causeway.core.config.CausewayConfiguration;
 import org.apache.causeway.testing.fixtures.applib.fixturescripts.FixtureScript;
 import org.apache.causeway.testing.fixtures.applib.fixturescripts.FixtureScripts;
@@ -48,15 +51,18 @@
 public class InitialFixtureScriptsInstaller {
 
     private final FixtureScripts fixtureScripts;
+    private final EventBusService eventBusService;
 
     private FixtureScript initialFixtureScript;
 
     @Inject
     public InitialFixtureScriptsInstaller(
             final CausewayConfiguration causewayConfiguration,
-            final FixtureScripts fixtureScripts) {
+            final FixtureScripts fixtureScripts,
+            final EventBusService eventBusService) {
 
         this.fixtureScripts = fixtureScripts;
+        this.eventBusService = eventBusService;
 
         final Class<?> initialScript = causewayConfiguration.testing().fixtures().initialScript();
         if (initialScript != null
@@ -79,7 +85,12 @@ public void onMetamodelEvent(final MetamodelEvent event) {
                 && initialFixtureScript != null) {
 
             log.info("install initial fixtures from script {}", initialFixtureScript.getFriendlyName());
-            fixtureScripts.run(initialFixtureScript);
+            try {
+                eventBusService.post(new PauseCommandLoggingEvent(this));
+                fixtureScripts.run(initialFixtureScript);
+            } finally {
+                eventBusService.post(new ResumeCommandLoggingEvent(this));
+            }
         }
     }
 
diff --git a/testing/fixtures/applib/src/test/java/org/apache/causeway/testing/fixtures/applib/services/InitialFixtureScriptsInstallerTest.java b/testing/fixtures/applib/src/test/java/org/apache/causeway/testing/fixtures/applib/services/InitialFixtureScriptsInstallerTest.java
new file mode 100644
index 00000000000..6fa7e10a6f9
--- /dev/null
+++ b/testing/fixtures/applib/src/test/java/org/apache/causeway/testing/fixtures/applib/services/InitialFixtureScriptsInstallerTest.java
@@ -0,0 +1,87 @@
+/*
+ *  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.testing.fixtures.applib.services;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.causeway.applib.events.metamodel.MetamodelEvent;
+import org.apache.causeway.applib.services.command.PauseCommandLoggingEvent;
+import org.apache.causeway.applib.services.command.ResumeCommandLoggingEvent;
+import org.apache.causeway.applib.services.eventbus.EventBusService;
+import org.apache.causeway.core.config.CausewayConfiguration;
+import org.apache.causeway.testing.fixtures.applib.fixturescripts.FixtureScript;
+import org.apache.causeway.testing.fixtures.applib.fixturescripts.FixtureScripts;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class InitialFixtureScriptsInstallerTest {
+
+    @Test
+    void publishesPauseRunsFixtureAndPublishesResumeInOrder() {
+        var fixtureScripts = mock(FixtureScripts.class);
+        var eventBusService = mock(EventBusService.class);
+        var installer = installer(fixtureScripts, eventBusService);
+
+        installer.onMetamodelEvent(MetamodelEvent.AFTER_METAMODEL_LOADED);
+
+        var inOrder = inOrder(eventBusService, fixtureScripts);
+        inOrder.verify(eventBusService).post(argThat(PauseCommandLoggingEvent.class::isInstance));
+        inOrder.verify(fixtureScripts).run(any(FixtureScript[].class));
+        inOrder.verify(eventBusService).post(argThat(ResumeCommandLoggingEvent.class::isInstance));
+    }
+
+    @Test
+    void publishesResumeWhenFixtureExecutionFails() {
+        var fixtureScripts = mock(FixtureScripts.class);
+        doThrow(new IllegalStateException("fixture failure"))
+            .when(fixtureScripts).run(any(FixtureScript[].class));
+        var eventBusService = mock(EventBusService.class);
+        var installer = installer(fixtureScripts, eventBusService);
+
+        assertThatThrownBy(() -> installer.onMetamodelEvent(MetamodelEvent.AFTER_METAMODEL_LOADED))
+            .isInstanceOf(IllegalStateException.class)
+            .hasMessage("fixture failure");
+
+        var inOrder = inOrder(eventBusService, fixtureScripts);
+        inOrder.verify(eventBusService).post(argThat(PauseCommandLoggingEvent.class::isInstance));
+        inOrder.verify(fixtureScripts).run(any(FixtureScript[].class));
+        inOrder.verify(eventBusService).post(argThat(ResumeCommandLoggingEvent.class::isInstance));
+    }
+
+    private InitialFixtureScriptsInstaller installer(
+            final FixtureScripts fixtureScripts,
+            final EventBusService eventBusService) {
+        var causewayConfiguration = mock(CausewayConfiguration.class);
+        var fixtures = new CausewayConfiguration.Testing.Fixtures(InitialScript.class, null);
+        when(causewayConfiguration.testing()).thenReturn(new CausewayConfiguration.Testing(fixtures));
+        return new InitialFixtureScriptsInstaller(causewayConfiguration, fixtureScripts, eventBusService);
+    }
+
+    public static class InitialScript extends FixtureScript {
+        @Override
+        protected void execute(final ExecutionContext executionContext) {
+        }
+    }
+}