diff --git a/Directory.Build.props b/Directory.Build.props
index 0582a71..ea51a65 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -8,7 +8,7 @@
- false
+ true
diff --git a/Directory.Packages.props b/Directory.Packages.props
index df62f0f..a62cdb4 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -2,7 +2,7 @@
true
-
+
@@ -29,18 +29,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/INDEX.md b/INDEX.md
index 48274c5..a373e53 100644
--- a/INDEX.md
+++ b/INDEX.md
@@ -12,6 +12,7 @@ Project overview: see [README](/README.md).
- Solution: [/src/Coven.sln](/src/Coven.sln)
- Core: [/src/Coven.Core](/src/Coven.Core/) ([README](/src/Coven.Core/README.md))
+- Core Debug: [/src/Coven.Core.Debug](/src/Coven.Core.Debug/) ([README](/src/Coven.Core.Debug/README.md))
- Streaming: [/src/Coven.Core.Streaming](/src/Coven.Core.Streaming/) ([README](/src/Coven.Core.Streaming/README.md))
- Daemonology: [/src/Coven.Daemonology](/src/Coven.Daemonology/) ([README](/src/Coven.Daemonology/README.md))
- Transmutation: [/src/Coven.Transmutation](/src/Coven.Transmutation/) ([README](/src/Coven.Transmutation/README.md))
diff --git a/src/Coven.Agents.OpenAI/ServiceCollectionExtensions.cs b/src/Coven.Agents.OpenAI/OpenAIAgentsServiceCollectionExtensions.cs
similarity index 98%
rename from src/Coven.Agents.OpenAI/ServiceCollectionExtensions.cs
rename to src/Coven.Agents.OpenAI/OpenAIAgentsServiceCollectionExtensions.cs
index 96f706c..cb42167 100644
--- a/src/Coven.Agents.OpenAI/ServiceCollectionExtensions.cs
+++ b/src/Coven.Agents.OpenAI/OpenAIAgentsServiceCollectionExtensions.cs
@@ -16,7 +16,7 @@ namespace Coven.Agents.OpenAI;
/// Dependency Injection helpers for wiring the OpenAI agent integration.
/// Registers journals, gateway connection, transmuters, windowing daemons, and the official OpenAI client.
///
-public static class ServiceCollectionExtensions
+public static class OpenAIAgentsServiceCollectionExtensions
{
///
/// Registers OpenAI agents with required defaults.
@@ -75,7 +75,7 @@ public static IServiceCollection AddOpenAIAgents(this IServiceCollection service
});
// Journals and gateway
- services.TryAddSingleton, InMemoryScrivener>();
+ services.TryAddScoped, InMemoryScrivener>();
services.AddKeyedScoped, InMemoryScrivener>("Coven.InternalOpenAIScrivener");
services.AddScoped, OpenAIScrivener>();
if (registration.StreamingEnabled)
diff --git a/src/Coven.Agents.OpenAI/OpenAIScrivener.cs b/src/Coven.Agents.OpenAI/OpenAIScrivener.cs
index f05f025..815a4fe 100644
--- a/src/Coven.Agents.OpenAI/OpenAIScrivener.cs
+++ b/src/Coven.Agents.OpenAI/OpenAIScrivener.cs
@@ -6,47 +6,46 @@
namespace Coven.Agents.OpenAI;
-internal sealed class OpenAIScrivener : IScrivener
+///
+/// OpenAI scrivener wrapper that forwards outbound efferent entries to the OpenAI gateway
+/// and persists all entries to the inner journal; logs the append for observability.
+///
+internal sealed class OpenAIScrivener : TappedScrivener
{
- private readonly IScrivener _inner;
private readonly IOpenAIGatewayConnection _gateway;
private readonly ILogger _logger;
+ ///
+ /// Creates an instance wrapping a keyed inner scrivener and an OpenAI gateway connection.
+ ///
+ /// The keyed inner scrivener used for storage.
+ /// Gateway connection for sending OpenAI requests.
+ /// Logger for diagnostic breadcrumbs.
public OpenAIScrivener(
[FromKeyedServices("Coven.InternalOpenAIScrivener")] IScrivener inner,
IOpenAIGatewayConnection gateway,
ILogger logger)
+ : base(inner)
{
- ArgumentNullException.ThrowIfNull(inner);
ArgumentNullException.ThrowIfNull(gateway);
ArgumentNullException.ThrowIfNull(logger);
- _inner = inner;
_gateway = gateway;
_logger = logger;
}
- public async Task WriteAsync(OpenAIEntry entry, CancellationToken cancellationToken = default)
+ ///
+ /// Sends entries via the gateway and appends all entries to the inner scrivener;
+ /// logs the append with the assigned position.
+ ///
+ public override async Task WriteAsync(OpenAIEntry entry, CancellationToken cancellationToken = default)
{
if (entry is OpenAIEfferent outgoing)
{
await _gateway.SendAsync(outgoing, cancellationToken).ConfigureAwait(false);
}
- long pos = await _inner.WriteAsync(entry, cancellationToken).ConfigureAwait(false);
+ long pos = await WriteInnerAsync(entry, cancellationToken).ConfigureAwait(false);
OpenAILog.OpenAIScrivenerAppended(_logger, entry.GetType().Name, pos);
return pos;
}
-
- public IAsyncEnumerable<(long journalPosition, OpenAIEntry entry)> TailAsync(long afterPosition = 0, CancellationToken cancellationToken = default)
- => _inner.TailAsync(afterPosition, cancellationToken);
-
- public IAsyncEnumerable<(long journalPosition, OpenAIEntry entry)> ReadBackwardAsync(long beforePosition = long.MaxValue, CancellationToken cancellationToken = default)
- => _inner.ReadBackwardAsync(beforePosition, cancellationToken);
-
- public Task<(long journalPosition, OpenAIEntry entry)> WaitForAsync(long afterPosition, Func match, CancellationToken cancellationToken = default)
- => _inner.WaitForAsync(afterPosition, match, cancellationToken);
-
- public Task<(long journalPosition, TDerived entry)> WaitForAsync(long afterPosition, Func match, CancellationToken cancellationToken = default)
- where TDerived : OpenAIEntry
- => _inner.WaitForAsync(afterPosition, match, cancellationToken);
}
diff --git a/src/Coven.Chat.Console/ServiceCollectionExtensions.cs b/src/Coven.Chat.Console/ConsoleChatServiceCollectionExtensions.cs
similarity index 92%
rename from src/Coven.Chat.Console/ServiceCollectionExtensions.cs
rename to src/Coven.Chat.Console/ConsoleChatServiceCollectionExtensions.cs
index d336090..d231fc5 100644
--- a/src/Coven.Chat.Console/ServiceCollectionExtensions.cs
+++ b/src/Coven.Chat.Console/ConsoleChatServiceCollectionExtensions.cs
@@ -1,10 +1,10 @@
// SPDX-License-Identifier: BUSL-1.1
using Coven.Core;
-using Microsoft.Extensions.DependencyInjection.Extensions;
using Coven.Daemonology;
using Coven.Transmutation;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Coven.Chat.Console;
@@ -12,7 +12,7 @@ namespace Coven.Chat.Console;
/// Dependency Injection helpers for wiring the Console chat adapter.
/// Registers gateway/session components, journals, the Console↔Chat transmuter, and the console daemon.
///
-public static class ServiceCollectionExtensions
+public static class ConsoleChatServiceCollectionExtensions
{
///
/// Adds Console chat integration using the provided client configuration.
@@ -29,7 +29,7 @@ public static IServiceCollection AddConsoleChat(this IServiceCollection services
services.AddScoped();
// Default ChatEntry journal if none provided by host
- services.TryAddSingleton, InMemoryScrivener>();
+ services.TryAddScoped, InMemoryScrivener>();
services.AddScoped, ConsoleScrivener>();
services.AddKeyedScoped, InMemoryScrivener>("Coven.InternalConsoleScrivener");
diff --git a/src/Coven.Chat.Console/ConsoleScrivener.cs b/src/Coven.Chat.Console/ConsoleScrivener.cs
index f08ef27..6ed0d98 100644
--- a/src/Coven.Chat.Console/ConsoleScrivener.cs
+++ b/src/Coven.Chat.Console/ConsoleScrivener.cs
@@ -6,9 +6,12 @@
namespace Coven.Chat.Console;
-internal sealed class ConsoleScrivener : IScrivener
+///
+/// Console chat scrivener wrapper that forwards outbound efferent entries to the console gateway
+/// and persists all entries to the inner journal for deterministic ordering and observation.
+///
+internal sealed class ConsoleScrivener : TappedScrivener
{
- private readonly IScrivener _scrivener;
private readonly ConsoleGatewayConnection _gateway;
private readonly ILogger _logger;
@@ -16,38 +19,27 @@ public ConsoleScrivener(
[FromKeyedServices("Coven.InternalConsoleScrivener")] IScrivener scrivener,
ConsoleGatewayConnection gateway,
ILogger logger)
+ : base(scrivener)
{
- ArgumentNullException.ThrowIfNull(scrivener);
ArgumentNullException.ThrowIfNull(gateway);
ArgumentNullException.ThrowIfNull(logger);
- _scrivener = scrivener;
_gateway = gateway;
_logger = logger;
}
- public async Task WriteAsync(ConsoleEntry entry, CancellationToken cancellationToken = default)
+ ///
+ /// Sends entries to the console gateway and appends
+ /// all entries to the inner scrivener; logs the append with the assigned position.
+ ///
+ public override async Task WriteAsync(ConsoleEntry entry, CancellationToken cancellationToken = default)
{
if (entry is ConsoleEfferent)
{
await _gateway.SendAsync(entry.Text, cancellationToken).ConfigureAwait(false);
}
- long pos = await _scrivener.WriteAsync(entry, cancellationToken).ConfigureAwait(false);
+ long pos = await WriteInnerAsync(entry, cancellationToken).ConfigureAwait(false);
ConsoleLog.ConsoleScrivenerAppended(_logger, entry.GetType().Name, pos);
return pos;
}
-
- public IAsyncEnumerable<(long journalPosition, ConsoleEntry entry)> TailAsync(long afterPosition = 0, CancellationToken cancellationToken = default)
- => _scrivener.TailAsync(afterPosition, cancellationToken);
-
- public IAsyncEnumerable<(long journalPosition, ConsoleEntry entry)> ReadBackwardAsync(long beforePosition = long.MaxValue, CancellationToken cancellationToken = default)
- => _scrivener.ReadBackwardAsync(beforePosition, cancellationToken);
-
- public Task<(long journalPosition, ConsoleEntry entry)> WaitForAsync(long afterPosition, Func match, CancellationToken cancellationToken = default)
- => _scrivener.WaitForAsync(afterPosition, match, cancellationToken);
-
- public Task<(long journalPosition, TDerived entry)> WaitForAsync(long afterPosition, Func match, CancellationToken cancellationToken = default)
- where TDerived : ConsoleEntry
- => _scrivener.WaitForAsync(afterPosition, match, cancellationToken);
}
-
diff --git a/src/Coven.Chat.Discord/ServiceCollectionExtensions.cs b/src/Coven.Chat.Discord/DiscordChatServiceCollectionExtensions.cs
similarity index 95%
rename from src/Coven.Chat.Discord/ServiceCollectionExtensions.cs
rename to src/Coven.Chat.Discord/DiscordChatServiceCollectionExtensions.cs
index eb8982d..a705208 100644
--- a/src/Coven.Chat.Discord/ServiceCollectionExtensions.cs
+++ b/src/Coven.Chat.Discord/DiscordChatServiceCollectionExtensions.cs
@@ -17,7 +17,7 @@ namespace Coven.Chat.Discord;
/// Dependency Injection helpers for wiring the Discord chat adapter.
/// Registers the Discord client, session factory, journals, transmuter, daemon, and default windowing policies.
///
-public static class ServiceCollectionExtensions
+public static class DiscordChatServiceCollectionExtensions
{
///
/// Adds Discord chat integration using the provided client configuration.
@@ -45,7 +45,7 @@ public static IServiceCollection AddDiscordChat(this IServiceCollection services
services.AddScoped();
// Default ChatEntry journal if none provided by host
- services.TryAddSingleton, InMemoryScrivener>();
+ services.TryAddScoped, InMemoryScrivener>();
services.AddScoped, DiscordScrivener>();
services.AddKeyedScoped, InMemoryScrivener>("Coven.InternalDiscordScrivener");
diff --git a/src/Coven.Chat.Discord/DiscordScrivener.cs b/src/Coven.Chat.Discord/DiscordScrivener.cs
index b6d24b2..ab8aec3 100644
--- a/src/Coven.Chat.Discord/DiscordScrivener.cs
+++ b/src/Coven.Chat.Discord/DiscordScrivener.cs
@@ -6,30 +6,36 @@
namespace Coven.Chat.Discord;
-internal sealed class DiscordScrivener : IScrivener
+///
+/// Discord chat scrivener wrapper that forwards outbound efferent entries to Discord via the gateway
+/// and persists all entries to the inner journal so pumps/tests can observe ordering.
+///
+internal sealed class DiscordScrivener : TappedScrivener
{
- private readonly IScrivener _scrivener;
private readonly DiscordGatewayConnection _discordClient;
private readonly ILogger _logger;
///
/// Wraps a keyed inner scrivener and forwards outbound efferent messages to Discord.
///
- /// The keyed inner used for storage.
+ /// The keyed inner scrivener used for storage.
/// The gateway connection for sending messages to Discord.
/// Logger for diagnostic breadcrumbs.
- /// Because we are what we utilize, ensure that the inner scrivener is keyed in DI.
+ /// Ensure the inner scrivener is keyed in DI.
public DiscordScrivener([FromKeyedServices("Coven.InternalDiscordScrivener")] IScrivener scrivener, DiscordGatewayConnection discordClient, ILogger logger)
+ : base(scrivener)
{
- ArgumentNullException.ThrowIfNull(scrivener);
ArgumentNullException.ThrowIfNull(discordClient);
ArgumentNullException.ThrowIfNull(logger);
- _scrivener = scrivener;
_discordClient = discordClient;
_logger = logger;
}
- public async Task WriteAsync(DiscordEntry entry, CancellationToken cancellationToken = default)
+ ///
+ /// Sends entries to Discord via the gateway and appends
+ /// all entries to the inner scrivener; logs the append with the assigned position.
+ ///
+ public override async Task WriteAsync(DiscordEntry entry, CancellationToken cancellationToken = default)
{
// Only send actual outbound messages to Discord. ACKs and inbound entries must not be sent.
if (entry is DiscordEfferent)
@@ -38,21 +44,8 @@ public async Task WriteAsync(DiscordEntry entry, CancellationToken cancell
}
// Always persist to the underlying scrivener so pumps and tests can observe ordering.
- long pos = await _scrivener.WriteAsync(entry, cancellationToken).ConfigureAwait(false);
+ long pos = await WriteInnerAsync(entry, cancellationToken).ConfigureAwait(false);
DiscordLog.DiscordScrivenerAppended(_logger, entry.GetType().Name, pos);
return pos;
}
-
- public IAsyncEnumerable<(long journalPosition, DiscordEntry entry)> TailAsync(long afterPosition = 0, CancellationToken cancellationToken = default)
- => _scrivener.TailAsync(afterPosition, cancellationToken);
-
- public IAsyncEnumerable<(long journalPosition, DiscordEntry entry)> ReadBackwardAsync(long beforePosition = long.MaxValue, CancellationToken cancellationToken = default)
- => _scrivener.ReadBackwardAsync(beforePosition, cancellationToken);
-
- public Task<(long journalPosition, DiscordEntry entry)> WaitForAsync(long afterPosition, Func match, CancellationToken cancellationToken = default)
- => _scrivener.WaitForAsync(afterPosition, match, cancellationToken);
-
- public Task<(long journalPosition, TDerived entry)> WaitForAsync(long afterPosition, Func match, CancellationToken cancellationToken = default)
- where TDerived : DiscordEntry
- => _scrivener.WaitForAsync(afterPosition, match, cancellationToken);
}
diff --git a/src/Coven.Chat/ServiceCollectionExtensions.cs b/src/Coven.Chat/ChatWindowingServiceCollectionExtensions.cs
similarity index 94%
rename from src/Coven.Chat/ServiceCollectionExtensions.cs
rename to src/Coven.Chat/ChatWindowingServiceCollectionExtensions.cs
index f76e83d..52f7704 100644
--- a/src/Coven.Chat/ServiceCollectionExtensions.cs
+++ b/src/Coven.Chat/ChatWindowingServiceCollectionExtensions.cs
@@ -13,7 +13,7 @@ namespace Coven.Chat;
/// Adds generic chat windowing infrastructure (journal, daemon) with a DI-provided window policy.
/// Useful for chunking and emitting grouped as entries.
///
-public static class ServiceCollectionExtensions
+public static class ChatWindowingServiceCollectionExtensions
{
///
/// Registers chat windowing components and a windowing daemon for .
@@ -26,7 +26,7 @@ public static IServiceCollection AddChatWindowing(this IServiceCollection servic
ArgumentNullException.ThrowIfNull(services);
// Ensure required journals exist
- services.TryAddSingleton, InMemoryScrivener>();
+ services.TryAddScoped, InMemoryScrivener>();
services.TryAddSingleton, InMemoryScrivener>();
// Register generic windowing daemon for Chat using a DI-provided policy
diff --git a/src/Coven.Core.Debug/Coven.Core.Debug.csproj b/src/Coven.Core.Debug/Coven.Core.Debug.csproj
new file mode 100644
index 0000000..2fcd78b
--- /dev/null
+++ b/src/Coven.Core.Debug/Coven.Core.Debug.csproj
@@ -0,0 +1,19 @@
+
+
+ net10.0
+ true
+ true
+ $(MSBuildProjectName)
+ Diagnostics helpers for Coven.Core (e.g., scrivener dump finalizer).
+ Coven
+ coven;debug;diagnostics;journal;dump
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Coven.Core.Debug/README.md b/src/Coven.Core.Debug/README.md
new file mode 100644
index 0000000..5f923dc
--- /dev/null
+++ b/src/Coven.Core.Debug/README.md
@@ -0,0 +1,25 @@
+# Coven.Core.Debug — Scrivener Taps
+
+Minimal, zero‑ceremony way to observe journal writes without changing any branch/leaf code: wrap an existing `IScrivener` with a tiny decorator that calls an observer delegate after each write.
+
+## Goal
+- Add an observer delegate to an existing scrivener.
+- Do not change `IScrivener`.
+- Do not write back to the observed journal.
+- Preserve normal tail/read behavior and positions.
+
+## Concept
+- `TappedScrivener` has an inner `IScrivener`.
+- On `WriteAsync(entry)`, it awaits the inner write to get the assigned `position`, then invokes an observer delegate: `Action`.
+- All read APIs (`TailAsync`, `ReadBackwardAsync`, `WaitForAsync`) simply delegate to the inner scrivener.
+
+## Behavior
+- Observation happens after the inner write completes, using the actual assigned position.
+- Observer exceptions are ignored (best‑effort, non‑interfering).
+- No writes back to the observed journal; tap is read‑only aside from the delegated call.
+- Overhead is a single delegate invocation per write.
+
+## Notes
+- Keep observers cheap; they run inline after writes. If you need heavy work, queue it yourself inside the delegate.
+- Because it wraps a specific IScrivener instance, it only observes entries written through that instance.
+- Because we need to register the TappedScrivener as the final implementation for IScrivener we will need some way to disambiguate them.
diff --git a/src/Coven.Core/Builder/ServiceCollectionExtensions.cs b/src/Coven.Core/Builder/CovenServiceBuilder.cs
similarity index 76%
rename from src/Coven.Core/Builder/ServiceCollectionExtensions.cs
rename to src/Coven.Core/Builder/CovenServiceBuilder.cs
index 4cb4dd7..bd2d163 100644
--- a/src/Coven.Core/Builder/ServiceCollectionExtensions.cs
+++ b/src/Coven.Core/Builder/CovenServiceBuilder.cs
@@ -4,47 +4,9 @@
using Coven.Core.Tags;
using Coven.Core.Activation;
using Coven.Core.Routing;
-using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Coven.Core.Builder;
-///
-/// DI entry points for composing and finalizing a Coven runtime.
-///
-public static class ServiceCollectionExtensions
-{
- ///
- /// Composes a Coven using the provided builder action and ensures finalization.
- ///
- /// The service collection.
- /// Callback to register MagikBlocks and options.
- /// The same service collection to enable fluent chaining.
- public static IServiceCollection BuildCoven(this IServiceCollection services, Action build)
- {
- ArgumentNullException.ThrowIfNull(services);
- ArgumentNullException.ThrowIfNull(build);
-
- CovenServiceBuilder builder = new(services);
- build(builder);
- // Idempotent finalize if user forgot to call Done()
- builder.Done();
- return services;
- }
-
- ///
- /// Registers a singleton in-memory scrivener for the given entry type.
- /// Uses TryAdd to avoid duplicate registrations.
- ///
- public static IServiceCollection AddInMemoryScrivener(this IServiceCollection services)
- where TEntry : notnull
- {
- ArgumentNullException.ThrowIfNull(services);
-
- services.TryAddSingleton, InMemoryScrivener>();
- return services;
- }
-}
-
///
/// Fluent builder used to register MagikBlocks and finalize the Coven runtime.
///
@@ -160,3 +122,4 @@ private void EnsureNotFinalized()
}
}
+
diff --git a/src/Coven.Core/Builder/CovenServiceCollectionExtensions.cs b/src/Coven.Core/Builder/CovenServiceCollectionExtensions.cs
new file mode 100644
index 0000000..c699f98
--- /dev/null
+++ b/src/Coven.Core/Builder/CovenServiceCollectionExtensions.cs
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: BUSL-1.1
+
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Coven.Core.Builder;
+
+///
+/// DI entry points for composing and finalizing a Coven runtime.
+///
+public static class CovenServiceCollectionExtensions
+{
+ ///
+ /// Composes a Coven using the provided builder action and ensures finalization.
+ ///
+ /// The service collection.
+ /// Callback to register MagikBlocks and options.
+ /// The same service collection to enable fluent chaining.
+ public static IServiceCollection BuildCoven(this IServiceCollection services, Action build)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(build);
+
+ CovenServiceBuilder builder = new(services);
+ build(builder);
+ // Idempotent finalize if user forgot to call Done()
+ builder.Done();
+ return services;
+ }
+}
+
diff --git a/src/Coven.Core/LambdaTappedScrivener.cs b/src/Coven.Core/LambdaTappedScrivener.cs
new file mode 100644
index 0000000..d2ad265
--- /dev/null
+++ b/src/Coven.Core/LambdaTappedScrivener.cs
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: BUSL-1.1
+
+namespace Coven.Core;
+
+///
+/// A tapped scrivener that uses a delegate to implement write behavior.
+///
+/// The journal entry type.
+///
+/// Initializes a new instance of the .
+///
+/// The inner scrivener used for storage and reads.
+/// Optional delegate that performs the write; receives the entry, the inner scrivener, and a token. Defaults to pass‑through to the inner.
+public sealed class LambdaTappedScrivener(
+ IScrivener inner,
+ Func, CancellationToken, Task>? write = null) : TappedScrivener(inner) where TEntry : notnull
+{
+ private readonly Func, CancellationToken, Task> _write = write ?? ((entry, innerScrivener, ct) => innerScrivener.WriteAsync(entry, ct));
+
+
+ ///
+ public override Task WriteAsync(TEntry entry, CancellationToken cancellationToken = default)
+ => _write(entry, Inner, cancellationToken);
+}
diff --git a/src/Coven.Core/TappedScrivener.cs b/src/Coven.Core/TappedScrivener.cs
new file mode 100644
index 0000000..0830551
--- /dev/null
+++ b/src/Coven.Core/TappedScrivener.cs
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: BUSL-1.1
+
+namespace Coven.Core;
+
+///
+/// Base scrivener wrapper that exposes the underlying scrivener while delegating
+/// read/tail/wait operations. Implementers override
+/// to perform side‑effects or routing, and can call
+/// to append to the inner journal while preserving ordering semantics.
+///
+/// The entry type for the journal.
+public abstract class TappedScrivener : IScrivener where TEntry : notnull
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The inner scrivener used for storage and read operations.
+ protected TappedScrivener(IScrivener inner)
+ {
+ ArgumentNullException.ThrowIfNull(inner);
+ Inner = inner;
+ }
+
+ ///
+ /// Implement write behavior for the tapped scrivener. Implementations may call
+ /// to append to the underlying journal
+ /// before or after performing side‑effects.
+ ///
+ /// The entry to append.
+ /// A cancellation token.
+ /// The assigned journal position.
+ public abstract Task WriteAsync(TEntry entry, CancellationToken cancellationToken = default);
+
+ ///
+ public IAsyncEnumerable<(long journalPosition, TEntry entry)> TailAsync(long afterPosition = 0, CancellationToken cancellationToken = default)
+ => Inner.TailAsync(afterPosition, cancellationToken);
+
+ ///
+ public IAsyncEnumerable<(long journalPosition, TEntry entry)> ReadBackwardAsync(long beforePosition = long.MaxValue, CancellationToken cancellationToken = default)
+ => Inner.ReadBackwardAsync(beforePosition, cancellationToken);
+
+ ///
+ public Task<(long journalPosition, TEntry entry)> WaitForAsync(long afterPosition, Func match, CancellationToken cancellationToken = default)
+ => Inner.WaitForAsync(afterPosition, match, cancellationToken);
+
+ ///
+ public Task<(long journalPosition, TDerived entry)> WaitForAsync(long afterPosition, Func match, CancellationToken cancellationToken = default)
+ where TDerived : TEntry
+ => Inner.WaitForAsync(afterPosition, match, cancellationToken);
+
+ /// Access the inner scrivener directly for advanced scenarios.
+ protected IScrivener Inner { get; }
+
+ ///
+ /// Append to the inner scrivener while preserving ordering and position semantics.
+ ///
+ protected Task WriteInnerAsync(TEntry entry, CancellationToken cancellationToken = default)
+ => Inner.WriteAsync(entry, cancellationToken);
+}
diff --git a/src/Coven.sln b/src/Coven.sln
index ca3e10e..1f0e511 100644
--- a/src/Coven.sln
+++ b/src/Coven.sln
@@ -43,6 +43,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Coven.Core.Streaming", "Cov
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Coven.Toys.DiscordStreaming", "toys\Coven.Toys.DiscordStreaming\Coven.Toys.DiscordStreaming.csproj", "{08F6A76C-CF62-4113-8740-CF5EF8115F99}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Coven.Core.Debug", "Coven.Core.Debug\Coven.Core.Debug.csproj", "{1264DB86-E00B-4222-B865-CF15BD752704}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -269,6 +271,18 @@ Global
{08F6A76C-CF62-4113-8740-CF5EF8115F99}.Release|x64.Build.0 = Release|Any CPU
{08F6A76C-CF62-4113-8740-CF5EF8115F99}.Release|x86.ActiveCfg = Release|Any CPU
{08F6A76C-CF62-4113-8740-CF5EF8115F99}.Release|x86.Build.0 = Release|Any CPU
+ {1264DB86-E00B-4222-B865-CF15BD752704}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1264DB86-E00B-4222-B865-CF15BD752704}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1264DB86-E00B-4222-B865-CF15BD752704}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1264DB86-E00B-4222-B865-CF15BD752704}.Debug|x64.Build.0 = Debug|Any CPU
+ {1264DB86-E00B-4222-B865-CF15BD752704}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1264DB86-E00B-4222-B865-CF15BD752704}.Debug|x86.Build.0 = Debug|Any CPU
+ {1264DB86-E00B-4222-B865-CF15BD752704}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1264DB86-E00B-4222-B865-CF15BD752704}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1264DB86-E00B-4222-B865-CF15BD752704}.Release|x64.ActiveCfg = Release|Any CPU
+ {1264DB86-E00B-4222-B865-CF15BD752704}.Release|x64.Build.0 = Release|Any CPU
+ {1264DB86-E00B-4222-B865-CF15BD752704}.Release|x86.ActiveCfg = Release|Any CPU
+ {1264DB86-E00B-4222-B865-CF15BD752704}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/samples/01.DiscordAgent/DiscordAgent.csproj b/src/samples/01.DiscordAgent/DiscordAgent.csproj
index 21d27d8..5f2e204 100644
--- a/src/samples/01.DiscordAgent/DiscordAgent.csproj
+++ b/src/samples/01.DiscordAgent/DiscordAgent.csproj
@@ -17,12 +17,13 @@
+
+
-