diff --git a/src/Persistence/MySql/Wolverine.MySql/AssemblyAttributes.cs b/src/Persistence/MySql/Wolverine.MySql/AssemblyAttributes.cs
index 225257b24..7c0f548e9 100644
--- a/src/Persistence/MySql/Wolverine.MySql/AssemblyAttributes.cs
+++ b/src/Persistence/MySql/Wolverine.MySql/AssemblyAttributes.cs
@@ -1,3 +1,6 @@
using System.Runtime.CompilerServices;
+using Wolverine.Attributes;
+
+[assembly: ExcludeFromServiceCapabilities]
[assembly: InternalsVisibleTo("MySqlTests")]
diff --git a/src/Persistence/Oracle/Wolverine.Oracle/AssemblyAttributes.cs b/src/Persistence/Oracle/Wolverine.Oracle/AssemblyAttributes.cs
index 4a0e5faa3..0ca6857d9 100644
--- a/src/Persistence/Oracle/Wolverine.Oracle/AssemblyAttributes.cs
+++ b/src/Persistence/Oracle/Wolverine.Oracle/AssemblyAttributes.cs
@@ -1,3 +1,6 @@
using System.Runtime.CompilerServices;
+using Wolverine.Attributes;
+
+[assembly: ExcludeFromServiceCapabilities]
[assembly: InternalsVisibleTo("OracleTests")]
diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/AssemblyAttributes.cs b/src/Persistence/Wolverine.EntityFrameworkCore/AssemblyAttributes.cs
index e30dfe826..838bb5005 100644
--- a/src/Persistence/Wolverine.EntityFrameworkCore/AssemblyAttributes.cs
+++ b/src/Persistence/Wolverine.EntityFrameworkCore/AssemblyAttributes.cs
@@ -1,4 +1,7 @@
using System.Runtime.CompilerServices;
+using Wolverine.Attributes;
+
+[assembly: ExcludeFromServiceCapabilities]
[assembly: InternalsVisibleTo("PersistenceTests")]
[assembly: InternalsVisibleTo("EfCoreTests")]
diff --git a/src/Persistence/Wolverine.Marten/AssemblyAttributes.cs b/src/Persistence/Wolverine.Marten/AssemblyAttributes.cs
index 2b47af08c..d2a647aac 100644
--- a/src/Persistence/Wolverine.Marten/AssemblyAttributes.cs
+++ b/src/Persistence/Wolverine.Marten/AssemblyAttributes.cs
@@ -1,4 +1,7 @@
using System.Runtime.CompilerServices;
+using Wolverine.Attributes;
+
+[assembly: ExcludeFromServiceCapabilities]
[assembly: InternalsVisibleTo("PersistenceTests")]
[assembly: InternalsVisibleTo("MartenTests")]
diff --git a/src/Persistence/Wolverine.Polecat/AssemblyAttributes.cs b/src/Persistence/Wolverine.Polecat/AssemblyAttributes.cs
index 90b6af158..9af68fa74 100644
--- a/src/Persistence/Wolverine.Polecat/AssemblyAttributes.cs
+++ b/src/Persistence/Wolverine.Polecat/AssemblyAttributes.cs
@@ -1,4 +1,7 @@
using System.Runtime.CompilerServices;
+using Wolverine.Attributes;
+
+[assembly: ExcludeFromServiceCapabilities]
[assembly: InternalsVisibleTo("PolecatTests")]
[assembly: InternalsVisibleTo("Wolverine.Http.Polecat")]
diff --git a/src/Persistence/Wolverine.Postgresql/AssemblyAttributes.cs b/src/Persistence/Wolverine.Postgresql/AssemblyAttributes.cs
index 2d7dc747b..6bba0fa92 100644
--- a/src/Persistence/Wolverine.Postgresql/AssemblyAttributes.cs
+++ b/src/Persistence/Wolverine.Postgresql/AssemblyAttributes.cs
@@ -1,4 +1,7 @@
using System.Runtime.CompilerServices;
+using Wolverine.Attributes;
+
+[assembly: ExcludeFromServiceCapabilities]
[assembly: InternalsVisibleTo("Wolverine.Marten")]
[assembly: InternalsVisibleTo("PersistenceTests")]
diff --git a/src/Persistence/Wolverine.RDBMS/AssemblyAttributes.cs b/src/Persistence/Wolverine.RDBMS/AssemblyAttributes.cs
index a67b2dbe9..e719eeda4 100644
--- a/src/Persistence/Wolverine.RDBMS/AssemblyAttributes.cs
+++ b/src/Persistence/Wolverine.RDBMS/AssemblyAttributes.cs
@@ -1,4 +1,7 @@
using System.Runtime.CompilerServices;
+using Wolverine.Attributes;
+
+[assembly: ExcludeFromServiceCapabilities]
[assembly: InternalsVisibleTo("PersistenceTests")]
[assembly: InternalsVisibleTo("SqlServerTests")]
diff --git a/src/Persistence/Wolverine.SqlServer/AssemblyAttributes.cs b/src/Persistence/Wolverine.SqlServer/AssemblyAttributes.cs
index b39a3407e..748aaec26 100644
--- a/src/Persistence/Wolverine.SqlServer/AssemblyAttributes.cs
+++ b/src/Persistence/Wolverine.SqlServer/AssemblyAttributes.cs
@@ -1,4 +1,7 @@
using System.Runtime.CompilerServices;
+using Wolverine.Attributes;
+
+[assembly: ExcludeFromServiceCapabilities]
[assembly: InternalsVisibleTo("PersistenceTests")]
[assembly: InternalsVisibleTo("SqlServerTests")]
\ No newline at end of file
diff --git a/src/Persistence/Wolverine.Sqlite/AssemblyAttributes.cs b/src/Persistence/Wolverine.Sqlite/AssemblyAttributes.cs
index 2c949e3fa..d8077b385 100644
--- a/src/Persistence/Wolverine.Sqlite/AssemblyAttributes.cs
+++ b/src/Persistence/Wolverine.Sqlite/AssemblyAttributes.cs
@@ -1,3 +1,6 @@
using System.Runtime.CompilerServices;
+using Wolverine.Attributes;
+
+[assembly: ExcludeFromServiceCapabilities]
[assembly: InternalsVisibleTo("SqliteTests")]
diff --git a/src/Testing/CoreTests/Acceptance/system_message_type_filtering.cs b/src/Testing/CoreTests/Acceptance/system_message_type_filtering.cs
new file mode 100644
index 000000000..55838c2a3
--- /dev/null
+++ b/src/Testing/CoreTests/Acceptance/system_message_type_filtering.cs
@@ -0,0 +1,152 @@
+using Microsoft.Extensions.Hosting;
+using Wolverine.Configuration;
+using Wolverine.Configuration.Capabilities;
+using Wolverine.ErrorHandling;
+using Wolverine.Logging;
+using Wolverine.Runtime;
+using Wolverine.Runtime.Agents;
+using Wolverine.Runtime.Metrics;
+using Wolverine.Runtime.Routing;
+using Wolverine.Tracking;
+using Wolverine.Transports;
+using Xunit;
+
+namespace CoreTests.Acceptance;
+
+///
+/// GH-2520: framework-internal message types must not leak into ServiceCapabilities
+/// or be reported via IWolverineObserver hooks (MessageRouted / MessageCausedBy).
+///
+public class system_message_type_filtering
+{
+ [Fact]
+ public void IsSystemMessageType_recognizes_IInternalMessage()
+ {
+ typeof(SampleInternalMessage).IsSystemMessageType().ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsSystemMessageType_recognizes_IAgentCommand()
+ {
+ typeof(SampleAgentCommand).IsSystemMessageType().ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsSystemMessageType_recognizes_INotToBeRouted()
+ {
+ typeof(SampleNotToBeRouted).IsSystemMessageType().ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsSystemMessageType_returns_false_for_normal_user_messages()
+ {
+ typeof(NormalUserMessage).IsSystemMessageType().ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsSystemMessageType_returns_false_for_null()
+ {
+ ((Type?)null).IsSystemMessageType().ShouldBeFalse();
+ }
+
+ [Fact]
+ public async Task service_capabilities_excludes_system_message_types()
+ {
+ using var host = await Host.CreateDefaultBuilder()
+ .UseWolverine(opts =>
+ {
+ opts.Discovery.DisableConventionalDiscovery();
+ opts.Discovery.IncludeType();
+ })
+ .StartAsync();
+
+ var capabilities = await ServiceCapabilities.ReadFrom(host.GetRuntime(), null, CancellationToken.None);
+
+ // Sanity: the normal user message should appear
+ capabilities.Messages.ShouldContain(m => m.Type.FullName == typeof(NormalUserMessage).FullName);
+
+ // The three system-marked types must NOT appear
+ capabilities.Messages.ShouldNotContain(m => m.Type.FullName == typeof(SampleInternalMessage).FullName);
+ capabilities.Messages.ShouldNotContain(m => m.Type.FullName == typeof(SampleAgentCommand).FullName);
+ capabilities.Messages.ShouldNotContain(m => m.Type.FullName == typeof(SampleNotToBeRouted).FullName);
+ }
+
+ [Fact]
+ public async Task observer_does_not_receive_message_routed_for_system_types()
+ {
+ using var host = await Host.CreateDefaultBuilder()
+ .UseWolverine(opts =>
+ {
+ opts.Discovery.DisableConventionalDiscovery();
+ opts.Discovery.IncludeType();
+ })
+ .StartAsync();
+
+ var runtime = host.GetRuntime();
+
+ // Swap in a recording observer after startup so we capture exactly the
+ // post-startup MessageRouted calls we care about.
+ var observer = new RecordingObserver();
+ runtime.Observer = observer;
+
+ // Force routing for each type so the observer hook is exercised
+ runtime.RoutingFor(typeof(NormalUserMessage));
+ runtime.RoutingFor(typeof(SampleInternalMessage));
+ runtime.RoutingFor(typeof(SampleAgentCommand));
+ runtime.RoutingFor(typeof(SampleNotToBeRouted));
+
+ // Normal type should be reported, system types should be filtered out
+ observer.RoutedTypes.ShouldContain(typeof(NormalUserMessage));
+ observer.RoutedTypes.ShouldNotContain(typeof(SampleInternalMessage));
+ observer.RoutedTypes.ShouldNotContain(typeof(SampleAgentCommand));
+ observer.RoutedTypes.ShouldNotContain(typeof(SampleNotToBeRouted));
+ }
+}
+
+// Fixtures live in the test (user) assembly to prove the per-type filter works
+// regardless of which assembly declares the type.
+public record NormalUserMessage(string Name);
+public record SampleInternalMessage(string Note) : IInternalMessage;
+public record SampleNotToBeRouted(string Note) : INotToBeRouted;
+
+public class SampleAgentCommand : IAgentCommand
+{
+ public Task ExecuteAsync(IWolverineRuntime runtime, CancellationToken cancellationToken)
+ => Task.FromResult(AgentCommands.Empty);
+}
+
+public class SystemFilteringHandler
+{
+ public void Handle(NormalUserMessage msg) { }
+ public void Handle(SampleInternalMessage msg) { }
+ public void Handle(SampleNotToBeRouted msg) { }
+ public void Handle(SampleAgentCommand cmd) { }
+}
+
+internal class RecordingObserver : IWolverineObserver
+{
+ public List RoutedTypes { get; } = new();
+
+ public void MessageRouted(Type messageType, IMessageRouter router)
+ {
+ RoutedTypes.Add(messageType);
+ }
+
+ // All other members are no-ops (interface uses default implementations)
+ public Task AssumedLeadership() => Task.CompletedTask;
+ public Task NodeStarted() => Task.CompletedTask;
+ public Task NodeStopped() => Task.CompletedTask;
+ public Task AgentStarted(Uri agentUri) => Task.CompletedTask;
+ public Task AgentStopped(Uri agentUri) => Task.CompletedTask;
+ public Task AssignmentsChanged(AssignmentGrid grid, AgentCommands commands) => Task.CompletedTask;
+ public Task StaleNodes(IReadOnlyList staleNodes) => Task.CompletedTask;
+ public Task RuntimeIsFullyStarted() => Task.CompletedTask;
+ public void EndpointAdded(Endpoint endpoint) { }
+ public Task BackPressureTriggered(Endpoint endpoint, IListeningAgent agent) => Task.CompletedTask;
+ public Task BackPressureLifted(Endpoint endpoint) => Task.CompletedTask;
+ public Task ListenerLatched(Endpoint endpoint) => Task.CompletedTask;
+ public Task CircuitBreakerTripped(Endpoint endpoint, CircuitBreakerOptions options) => Task.CompletedTask;
+ public Task CircuitBreakerReset(Endpoint endpoint) => Task.CompletedTask;
+ public void PersistedCounts(Uri storeUri, PersistedCounts counts) { }
+ public void MessageHandlingMetricsExported(MessageHandlingMetrics metrics) { }
+}
diff --git a/src/Wolverine/Configuration/Capabilities/ServiceCapabilities.cs b/src/Wolverine/Configuration/Capabilities/ServiceCapabilities.cs
index ada108cc2..273df6922 100644
--- a/src/Wolverine/Configuration/Capabilities/ServiceCapabilities.cs
+++ b/src/Wolverine/Configuration/Capabilities/ServiceCapabilities.cs
@@ -2,7 +2,6 @@
using System.Text.Json.Serialization;
using JasperFx.Core.Reflection;
using JasperFx.Descriptors;
-using Wolverine.Attributes;
using JasperFx.Events;
using JasperFx.Events.Descriptors;
using Microsoft.Extensions.DependencyInjection;
@@ -107,7 +106,7 @@ private static void readMessageTypes(IWolverineRuntime runtime, ServiceCapabilit
var messageTypes = runtime.Options.Discovery.FindAllMessages(runtime.Options.HandlerGraph);
foreach (var messageType in messageTypes.OrderBy(x => x.FullNameInCode()))
{
- if (messageType.Assembly.HasAttribute()) continue;
+ if (messageType.IsSystemMessageType()) continue;
capabilities.Messages.Add(new MessageDescriptor(messageType, runtime));
}
}
diff --git a/src/Wolverine/Configuration/Capabilities/SystemMessageTypeExtensions.cs b/src/Wolverine/Configuration/Capabilities/SystemMessageTypeExtensions.cs
new file mode 100644
index 000000000..1abe3ac23
--- /dev/null
+++ b/src/Wolverine/Configuration/Capabilities/SystemMessageTypeExtensions.cs
@@ -0,0 +1,41 @@
+using JasperFx.Core.Reflection;
+using Wolverine.Attributes;
+using Wolverine.Runtime;
+using Wolverine.Runtime.Agents;
+
+namespace Wolverine.Configuration.Capabilities;
+
+///
+/// Centralized predicate for deciding whether a given message type is a system /
+/// framework-internal type that should be excluded from observability surfaces:
+/// , IWolverineObserver.MessageRouted, and
+/// IWolverineObserver.MessageCausedBy.
+///
+internal static class SystemMessageTypeExtensions
+{
+ ///
+ /// Returns true if the supplied message type should be hidden from observability.
+ /// Catches:
+ ///
+ /// - Types implementing (preferred — fastest)
+ /// - Types implementing (Wolverine internal commands)
+ /// - Types implementing (covers ,
+ /// ICritterWatchMessage, acknowledgements, etc.)
+ /// - Types declared in an assembly marked with
+ ///
+ ///
+ public static bool IsSystemMessageType(this Type? messageType)
+ {
+ if (messageType is null) return false;
+
+ // Marker-interface checks first — these are constant-time runtime type tests.
+ if (messageType.CanBeCastTo()) return true;
+ if (messageType.CanBeCastTo()) return true;
+ if (messageType.CanBeCastTo()) return true;
+
+ // Assembly-level opt-out — slower, falls through to reflection but cached by the runtime.
+ if (messageType.Assembly.HasAttribute()) return true;
+
+ return false;
+ }
+}
diff --git a/src/Wolverine/Runtime/Handlers/MessageHandler.cs b/src/Wolverine/Runtime/Handlers/MessageHandler.cs
index 9b971238a..66698f32a 100644
--- a/src/Wolverine/Runtime/Handlers/MessageHandler.cs
+++ b/src/Wolverine/Runtime/Handlers/MessageHandler.cs
@@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using JasperFx.Core.Reflection;
using Microsoft.Extensions.Logging;
+using Wolverine.Configuration.Capabilities;
using Wolverine.Runtime.Agents;
namespace Wolverine.Runtime.Handlers;
@@ -59,13 +60,26 @@ public void RecordCauseAndEffect(MessageContext context, IWolverineObserver obse
{
if (!context.Runtime.Options.EnableMessageCausationTracking) return;
+ // Skip the entire causation report when the incoming message itself is a
+ // framework-internal type (IAgentCommand, INotToBeRouted, IInternalMessage, etc.).
+ // See GH-2520.
+ if (MessageType.IsSystemMessageType()) return;
+
var incomingType = MessageType.FullName ?? MessageType.Name;
var handlerType = GetType().FullName ?? GetType().Name;
var endpointUri = Chain?.Endpoints?.FirstOrDefault()?.Uri?.ToString();
foreach (var envelope in context.Outstanding)
{
- var outgoingType = envelope.Message?.GetType().FullName;
+ var outgoingMessage = envelope.Message;
+ if (outgoingMessage is null) continue;
+
+ // Per-instance check uses fast pattern match over runtime type-tests for
+ // the marker interfaces; falls through to the helper for assembly attrs.
+ var outgoingMessageType = outgoingMessage.GetType();
+ if (outgoingMessageType.IsSystemMessageType()) continue;
+
+ var outgoingType = outgoingMessageType.FullName;
if (string.IsNullOrEmpty(outgoingType)) continue;
var key = $"{incomingType}->{outgoingType}@{handlerType}";
diff --git a/src/Wolverine/Runtime/IInternalMessage.cs b/src/Wolverine/Runtime/IInternalMessage.cs
new file mode 100644
index 000000000..75fc4e322
--- /dev/null
+++ b/src/Wolverine/Runtime/IInternalMessage.cs
@@ -0,0 +1,15 @@
+namespace Wolverine.Runtime;
+
+///
+/// Marker interface for message types that are internal to Wolverine or to extension
+/// frameworks (such as CritterWatch). Types implementing this interface are excluded
+/// from and
+/// from IWolverineObserver notifications such as MessageRouted and
+/// MessageCausedBy.
+///
+/// Use this for system commands and infrastructure messages that should not appear in
+/// observability tooling alongside user-defined messages. Per-instance filtering uses
+/// a single type-test (x is IInternalMessage), which is cheaper than reflection
+/// or attribute lookups.
+///
+public interface IInternalMessage;
diff --git a/src/Wolverine/Runtime/WolverineRuntime.Routing.cs b/src/Wolverine/Runtime/WolverineRuntime.Routing.cs
index 1a07520ba..856933287 100644
--- a/src/Wolverine/Runtime/WolverineRuntime.Routing.cs
+++ b/src/Wolverine/Runtime/WolverineRuntime.Routing.cs
@@ -2,6 +2,7 @@
using JasperFx.Core;
using JasperFx.Core.Reflection;
using Wolverine.Configuration;
+using Wolverine.Configuration.Capabilities;
using Wolverine.Runtime.Agents;
using Wolverine.Runtime.Routing;
using Wolverine.Transports;
@@ -155,7 +156,13 @@ public IMessageRouter RoutingFor(Type messageType)
? typeof(MessageRouter<>).CloseAndBuildAs(this, routes, messageType)
: typeof(EmptyMessageRouter<>).CloseAndBuildAs(this, messageType);
- Observer.MessageRouted(messageType, router);
+ // Skip framework-internal types (IAgentCommand, INotToBeRouted, IInternalMessage,
+ // and types from assemblies marked [ExcludeFromServiceCapabilities]) so they
+ // never reach observers like CritterWatch. See GH-2520.
+ if (!messageType.IsSystemMessageType())
+ {
+ Observer.MessageRouted(messageType, router);
+ }
_messageTypeRouting = _messageTypeRouting.AddOrUpdate(messageType, router);