diff --git a/src/ScadaLink.AuditLog/Site/Telemetry/CachedCallLifecycleBridge.cs b/src/ScadaLink.AuditLog/Site/Telemetry/CachedCallLifecycleBridge.cs
new file mode 100644
index 0000000..d61c3fb
--- /dev/null
+++ b/src/ScadaLink.AuditLog/Site/Telemetry/CachedCallLifecycleBridge.cs
@@ -0,0 +1,205 @@
+using Microsoft.Extensions.Logging;
+using ScadaLink.Commons.Entities.Audit;
+using ScadaLink.Commons.Interfaces.Services;
+using ScadaLink.Commons.Messages.Integration;
+using ScadaLink.Commons.Types;
+using ScadaLink.Commons.Types.Enums;
+
+namespace ScadaLink.AuditLog.Site.Telemetry;
+
+///
+/// Audit Log #23 (M3 Bundle E — Tasks E4/E5): translates per-attempt
+/// notifications from the store-and-forward retry loop into one (or two)
+/// packets and pushes them through
+/// .
+///
+///
+///
+/// The S&F loop's reports a
+/// single coarse outcome per attempt; the audit pipeline however models the
+/// lifecycle as TWO rows on terminal outcomes — an Attempted
+/// ( / )
+/// row capturing the per-attempt mechanics, plus a
+/// row marking the terminal state for downstream consumers. The bridge fans
+/// out per outcome:
+///
+///
+/// - TransientFailure -> one Attempted(Failed) row.
+/// - Delivered -> Attempted(Delivered) + CachedResolve(Delivered).
+/// - PermanentFailure -> Attempted(Failed) + CachedResolve(Parked).
+/// - ParkedMaxRetries -> Attempted(Failed) + CachedResolve(Parked).
+///
+///
+/// Best-effort emission (alog.md §7): the bridge itself never throws;
+/// the underlying forwarder swallows + logs its own failures.
+///
+///
+public sealed class CachedCallLifecycleBridge : ICachedCallLifecycleObserver
+{
+ private readonly ICachedCallTelemetryForwarder _forwarder;
+ private readonly ILogger _logger;
+
+ public CachedCallLifecycleBridge(
+ ICachedCallTelemetryForwarder forwarder,
+ ILogger logger)
+ {
+ _forwarder = forwarder ?? throw new ArgumentNullException(nameof(forwarder));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ public async Task OnAttemptCompletedAsync(
+ CachedCallAttemptContext context, CancellationToken ct = default)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+
+ try
+ {
+ await EmitAttemptedAsync(context, ct).ConfigureAwait(false);
+
+ if (IsTerminal(context.Outcome))
+ {
+ await EmitResolveAsync(context, ct).ConfigureAwait(false);
+ }
+ }
+ catch (Exception ex)
+ {
+ // Defensive — both EmitX paths call the forwarder which is itself
+ // best-effort. A throw here is unexpected, but the alog.md §7
+ // contract requires we never propagate.
+ _logger.LogWarning(ex,
+ "CachedCallLifecycleBridge: unexpected throw for {TrackedOperationId} (Outcome {Outcome})",
+ context.TrackedOperationId, context.Outcome);
+ }
+ }
+
+ private async Task EmitAttemptedAsync(CachedCallAttemptContext context, CancellationToken ct)
+ {
+ // Per-attempt row: kind discriminates channel, status carries the
+ // per-attempt result.
+ var kind = ChannelToAttemptKind(context.Channel);
+ var status = context.Outcome == CachedCallAttemptOutcome.Delivered
+ ? AuditStatus.Attempted
+ : AuditStatus.Attempted;
+ // (Note: per the M3 brief and alog.md §4 the per-attempt row always
+ // carries Status=Attempted; success vs. failure is captured by the
+ // companion HttpStatus / ErrorMessage fields, NOT by flipping the
+ // status. CachedResolve carries the terminal Status.)
+
+ var packet = BuildPacket(
+ context,
+ kind: kind,
+ status: status,
+ // Operational status mirror — for the per-attempt row the
+ // operational state is the running status; the bridge always
+ // writes "Attempted" so reconciliation can't roll back.
+ operationalStatus: "Attempted",
+ terminalAtUtc: null,
+ lastError: context.LastError,
+ httpStatus: context.HttpStatus);
+
+ await _forwarder.ForwardAsync(packet, ct).ConfigureAwait(false);
+ }
+
+ private async Task EmitResolveAsync(CachedCallAttemptContext context, CancellationToken ct)
+ {
+ var (auditStatus, operationalStatus) = TerminalOutcomeToStatuses(context.Outcome);
+
+ var packet = BuildPacket(
+ context,
+ kind: AuditKind.CachedResolve,
+ status: auditStatus,
+ operationalStatus: operationalStatus,
+ terminalAtUtc: context.OccurredAtUtc,
+ lastError: context.LastError,
+ httpStatus: context.HttpStatus);
+
+ await _forwarder.ForwardAsync(packet, ct).ConfigureAwait(false);
+ }
+
+ private static CachedCallTelemetry BuildPacket(
+ CachedCallAttemptContext context,
+ AuditKind kind,
+ AuditStatus status,
+ string operationalStatus,
+ DateTime? terminalAtUtc,
+ string? lastError,
+ int? httpStatus)
+ {
+ var channel = ChannelStringToEnum(context.Channel);
+
+ return new CachedCallTelemetry(
+ Audit: new AuditEvent
+ {
+ EventId = Guid.NewGuid(),
+ OccurredAtUtc = DateTime.SpecifyKind(context.OccurredAtUtc, DateTimeKind.Utc),
+ Channel = channel,
+ Kind = kind,
+ CorrelationId = context.TrackedOperationId.Value,
+ SourceSiteId = string.IsNullOrEmpty(context.SourceSite) ? null : context.SourceSite,
+ SourceInstanceId = context.SourceInstanceId,
+ SourceScript = null, // Not threaded through S&F; left null on retry-loop rows.
+ Target = context.Target,
+ Status = status,
+ HttpStatus = httpStatus,
+ DurationMs = context.DurationMs,
+ ErrorMessage = lastError,
+ ForwardState = AuditForwardState.Pending,
+ },
+ Operational: new SiteCallOperational(
+ TrackedOperationId: context.TrackedOperationId,
+ Channel: context.Channel,
+ Target: context.Target,
+ SourceSite: context.SourceSite,
+ Status: operationalStatus,
+ RetryCount: context.RetryCount,
+ LastError: lastError,
+ HttpStatus: httpStatus,
+ CreatedAtUtc: DateTime.SpecifyKind(context.CreatedAtUtc, DateTimeKind.Utc),
+ UpdatedAtUtc: DateTime.SpecifyKind(context.OccurredAtUtc, DateTimeKind.Utc),
+ TerminalAtUtc: terminalAtUtc is null
+ ? null
+ : DateTime.SpecifyKind(terminalAtUtc.Value, DateTimeKind.Utc)));
+ }
+
+ private static AuditKind ChannelToAttemptKind(string channel) => channel switch
+ {
+ "ApiOutbound" => AuditKind.ApiCallCached,
+ "DbOutbound" => AuditKind.DbWriteCached,
+ // Defensive default — the S&F observer is filtered to cached-call
+ // categories so this branch shouldn't fire in practice.
+ _ => AuditKind.ApiCallCached,
+ };
+
+ private static AuditChannel ChannelStringToEnum(string channel) => channel switch
+ {
+ "ApiOutbound" => AuditChannel.ApiOutbound,
+ "DbOutbound" => AuditChannel.DbOutbound,
+ _ => AuditChannel.ApiOutbound,
+ };
+
+ private static (AuditStatus auditStatus, string operationalStatus) TerminalOutcomeToStatuses(
+ CachedCallAttemptOutcome outcome) => outcome switch
+ {
+ CachedCallAttemptOutcome.Delivered =>
+ (AuditStatus.Delivered, "Delivered"),
+ CachedCallAttemptOutcome.PermanentFailure =>
+ (AuditStatus.Parked, "Parked"),
+ CachedCallAttemptOutcome.ParkedMaxRetries =>
+ (AuditStatus.Parked, "Parked"),
+ // TransientFailure isn't terminal — see IsTerminal — but the switch
+ // is exhaustive so we route it through Failed for safety.
+ CachedCallAttemptOutcome.TransientFailure =>
+ (AuditStatus.Failed, "Failed"),
+ _ => (AuditStatus.Failed, "Failed"),
+ };
+
+ private static bool IsTerminal(CachedCallAttemptOutcome outcome) => outcome switch
+ {
+ CachedCallAttemptOutcome.Delivered => true,
+ CachedCallAttemptOutcome.PermanentFailure => true,
+ CachedCallAttemptOutcome.ParkedMaxRetries => true,
+ CachedCallAttemptOutcome.TransientFailure => false,
+ _ => false,
+ };
+}
diff --git a/tests/ScadaLink.AuditLog.Tests/Site/Telemetry/CachedCallLifecycleBridgeTests.cs b/tests/ScadaLink.AuditLog.Tests/Site/Telemetry/CachedCallLifecycleBridgeTests.cs
new file mode 100644
index 0000000..97cbb84
--- /dev/null
+++ b/tests/ScadaLink.AuditLog.Tests/Site/Telemetry/CachedCallLifecycleBridgeTests.cs
@@ -0,0 +1,187 @@
+using Microsoft.Extensions.Logging.Abstractions;
+using NSubstitute;
+using ScadaLink.AuditLog.Site.Telemetry;
+using ScadaLink.Commons.Interfaces.Services;
+using ScadaLink.Commons.Messages.Integration;
+using ScadaLink.Commons.Types;
+using ScadaLink.Commons.Types.Enums;
+
+namespace ScadaLink.AuditLog.Tests.Site.Telemetry;
+
+///
+/// Bundle E Tasks E4/E5 bridge tests. The bridge ingests
+/// notifications from the S&F
+/// retry loop and routes them through
+/// as one or two packets:
+///
+/// - Per-attempt: one ApiCallCached/DbWriteCached Attempted row.
+/// - Terminal (Delivered/PermanentFailure/ParkedMaxRetries): adds a CachedResolve row carrying the terminal Status.
+///
+///
+public class CachedCallLifecycleBridgeTests
+{
+ private readonly ICachedCallTelemetryForwarder _forwarder = Substitute.For();
+ private readonly TrackedOperationId _id = TrackedOperationId.New();
+
+ private CachedCallLifecycleBridge CreateSut() => new(
+ _forwarder, NullLogger.Instance);
+
+ private CachedCallAttemptContext Ctx(
+ CachedCallAttemptOutcome outcome,
+ string channel = "ApiOutbound",
+ int retryCount = 1,
+ string? lastError = null,
+ int? httpStatus = null) =>
+ new(
+ TrackedOperationId: _id,
+ Channel: channel,
+ Target: "ERP.GetOrder",
+ SourceSite: "site-77",
+ Outcome: outcome,
+ RetryCount: retryCount,
+ LastError: lastError,
+ HttpStatus: httpStatus,
+ CreatedAtUtc: new DateTime(2026, 5, 20, 9, 0, 0, DateTimeKind.Utc),
+ OccurredAtUtc: new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc),
+ DurationMs: 42,
+ SourceInstanceId: "Plant.Pump42");
+
+ [Fact]
+ public async Task TransientFailure_EmitsOneAttemptedRow_NoResolve()
+ {
+ var captured = new List();
+ _forwarder.ForwardAsync(Arg.Do(t => captured.Add(t)), Arg.Any())
+ .Returns(Task.CompletedTask);
+
+ var sut = CreateSut();
+ await sut.OnAttemptCompletedAsync(Ctx(
+ CachedCallAttemptOutcome.TransientFailure,
+ retryCount: 2,
+ lastError: "HTTP 503",
+ httpStatus: 503));
+
+ var packet = Assert.Single(captured);
+ Assert.Equal(AuditKind.ApiCallCached, packet.Audit.Kind);
+ Assert.Equal(AuditStatus.Attempted, packet.Audit.Status);
+ Assert.Equal(503, packet.Audit.HttpStatus);
+ Assert.Equal("HTTP 503", packet.Audit.ErrorMessage);
+ Assert.Equal(_id.Value, packet.Audit.CorrelationId);
+ Assert.Equal("Attempted", packet.Operational.Status);
+ Assert.Equal(2, packet.Operational.RetryCount);
+ Assert.Null(packet.Operational.TerminalAtUtc);
+ }
+
+ [Fact]
+ public async Task Delivered_EmitsAttemptedRow_AndCachedResolveDelivered()
+ {
+ var captured = new List();
+ _forwarder.ForwardAsync(Arg.Do(t => captured.Add(t)), Arg.Any())
+ .Returns(Task.CompletedTask);
+
+ var sut = CreateSut();
+ await sut.OnAttemptCompletedAsync(Ctx(CachedCallAttemptOutcome.Delivered));
+
+ Assert.Equal(2, captured.Count);
+
+ var attempted = captured[0];
+ Assert.Equal(AuditKind.ApiCallCached, attempted.Audit.Kind);
+ Assert.Equal(AuditStatus.Attempted, attempted.Audit.Status);
+ Assert.Equal("Attempted", attempted.Operational.Status);
+ Assert.Null(attempted.Operational.TerminalAtUtc);
+
+ var resolve = captured[1];
+ Assert.Equal(AuditKind.CachedResolve, resolve.Audit.Kind);
+ Assert.Equal(AuditStatus.Delivered, resolve.Audit.Status);
+ Assert.Equal("Delivered", resolve.Operational.Status);
+ Assert.NotNull(resolve.Operational.TerminalAtUtc);
+ Assert.Equal(_id.Value, resolve.Audit.CorrelationId);
+ }
+
+ [Fact]
+ public async Task PermanentFailure_EmitsAttempted_AndCachedResolveParked()
+ {
+ var captured = new List();
+ _forwarder.ForwardAsync(Arg.Do(t => captured.Add(t)), Arg.Any())
+ .Returns(Task.CompletedTask);
+
+ var sut = CreateSut();
+ await sut.OnAttemptCompletedAsync(Ctx(
+ CachedCallAttemptOutcome.PermanentFailure,
+ lastError: "Permanent failure (handler returned false)"));
+
+ Assert.Equal(2, captured.Count);
+ Assert.Equal(AuditKind.ApiCallCached, captured[0].Audit.Kind);
+ Assert.Equal(AuditKind.CachedResolve, captured[1].Audit.Kind);
+ Assert.Equal(AuditStatus.Parked, captured[1].Audit.Status);
+ Assert.Equal("Parked", captured[1].Operational.Status);
+ }
+
+ [Fact]
+ public async Task ParkedMaxRetries_EmitsAttempted_AndCachedResolveParked()
+ {
+ var captured = new List();
+ _forwarder.ForwardAsync(Arg.Do(t => captured.Add(t)), Arg.Any())
+ .Returns(Task.CompletedTask);
+
+ var sut = CreateSut();
+ await sut.OnAttemptCompletedAsync(Ctx(CachedCallAttemptOutcome.ParkedMaxRetries));
+
+ Assert.Equal(2, captured.Count);
+ Assert.Equal(AuditKind.CachedResolve, captured[1].Audit.Kind);
+ Assert.Equal(AuditStatus.Parked, captured[1].Audit.Status);
+ }
+
+ [Fact]
+ public async Task DbChannel_MapsToDbWriteCachedKind_AndDbOutboundChannel()
+ {
+ var captured = new List();
+ _forwarder.ForwardAsync(Arg.Do(t => captured.Add(t)), Arg.Any())
+ .Returns(Task.CompletedTask);
+
+ var sut = CreateSut();
+ await sut.OnAttemptCompletedAsync(Ctx(
+ CachedCallAttemptOutcome.Delivered, channel: "DbOutbound"));
+
+ Assert.Equal(2, captured.Count);
+ Assert.Equal(AuditKind.DbWriteCached, captured[0].Audit.Kind);
+ Assert.Equal(AuditChannel.DbOutbound, captured[0].Audit.Channel);
+ Assert.Equal("DbOutbound", captured[0].Operational.Channel);
+ Assert.Equal(AuditKind.CachedResolve, captured[1].Audit.Kind);
+ Assert.Equal(AuditChannel.DbOutbound, captured[1].Audit.Channel);
+ }
+
+ [Fact]
+ public async Task BridgeDoesNotThrow_WhenForwarderThrows()
+ {
+ _forwarder
+ .ForwardAsync(Arg.Any(), Arg.Any())
+ .Returns(Task.FromException(new InvalidOperationException("forwarder down")));
+
+ var sut = CreateSut();
+
+ // Must not throw — best-effort emission.
+ await sut.OnAttemptCompletedAsync(Ctx(CachedCallAttemptOutcome.Delivered));
+ }
+
+ [Fact]
+ public async Task BridgePopulatesProvenance_FromAttemptContext()
+ {
+ CachedCallTelemetry? captured = null;
+ _forwarder.ForwardAsync(Arg.Do(t => captured = t), Arg.Any())
+ .Returns(Task.CompletedTask);
+
+ var sut = CreateSut();
+ await sut.OnAttemptCompletedAsync(Ctx(
+ CachedCallAttemptOutcome.TransientFailure,
+ retryCount: 3,
+ lastError: "transient",
+ httpStatus: 500));
+
+ Assert.NotNull(captured);
+ Assert.Equal("site-77", captured!.Audit.SourceSiteId);
+ Assert.Equal("Plant.Pump42", captured.Audit.SourceInstanceId);
+ Assert.Equal("ERP.GetOrder", captured.Audit.Target);
+ Assert.Equal(42, captured.Audit.DurationMs);
+ Assert.Equal(_id.Value, captured.Audit.CorrelationId);
+ }
+}
diff --git a/tests/ScadaLink.SiteRuntime.Tests/Scripts/DatabaseCachedWriteEmissionTests.cs b/tests/ScadaLink.SiteRuntime.Tests/Scripts/DatabaseCachedWriteEmissionTests.cs
new file mode 100644
index 0000000..991bbaf
--- /dev/null
+++ b/tests/ScadaLink.SiteRuntime.Tests/Scripts/DatabaseCachedWriteEmissionTests.cs
@@ -0,0 +1,170 @@
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using ScadaLink.Commons.Entities.Audit;
+using ScadaLink.Commons.Interfaces.Services;
+using ScadaLink.Commons.Messages.Integration;
+using ScadaLink.Commons.Types;
+using ScadaLink.Commons.Types.Enums;
+using ScadaLink.SiteRuntime.Scripts;
+
+namespace ScadaLink.SiteRuntime.Tests.Scripts;
+
+///
+/// Audit Log #23 — M3 Bundle E (Task E6): every script-initiated
+/// Database.CachedWrite emits exactly one CachedSubmit
+/// combined-telemetry packet at enqueue time on the DbOutbound
+/// channel, returns a fresh , and threads
+/// the id into the database gateway so the store-and-forward retry loop can
+/// emit per-attempt + terminal telemetry under the same id.
+///
+public class DatabaseCachedWriteEmissionTests
+{
+ private sealed class CapturingForwarder : ICachedCallTelemetryForwarder
+ {
+ public List Telemetry { get; } = new();
+ public Exception? ThrowOnForward { get; set; }
+
+ public Task ForwardAsync(CachedCallTelemetry telemetry, CancellationToken ct = default)
+ {
+ if (ThrowOnForward != null)
+ {
+ return Task.FromException(ThrowOnForward);
+ }
+ Telemetry.Add(telemetry);
+ return Task.CompletedTask;
+ }
+ }
+
+ private const string SiteId = "site-77";
+ private const string InstanceName = "Plant.Pump42";
+ private const string SourceScript = "ScriptActor:WriteAudit";
+
+ private static ScriptRuntimeContext.DatabaseHelper CreateHelper(
+ IDatabaseGateway gateway,
+ ICachedCallTelemetryForwarder? forwarder)
+ {
+ return new ScriptRuntimeContext.DatabaseHelper(
+ gateway,
+ InstanceName,
+ NullLogger.Instance,
+ siteId: SiteId,
+ sourceScript: SourceScript,
+ cachedForwarder: forwarder);
+ }
+
+ [Fact]
+ public async Task CachedWrite_EmitsSubmitTelemetry_OnEnqueue_KindCachedSubmit_ChannelDbOutbound()
+ {
+ var gateway = new Mock();
+ gateway
+ .Setup(g => g.CachedWriteAsync(
+ "myDb", "INSERT INTO t VALUES (1)",
+ It.IsAny?>(),
+ InstanceName,
+ It.IsAny(),
+ It.IsAny()))
+ .Returns(Task.CompletedTask);
+ var forwarder = new CapturingForwarder();
+
+ var helper = CreateHelper(gateway.Object, forwarder);
+ var trackedId = await helper.CachedWrite("myDb", "INSERT INTO t VALUES (1)");
+
+ Assert.NotEqual(default, trackedId);
+ var packet = Assert.Single(forwarder.Telemetry);
+
+ Assert.Equal(AuditChannel.DbOutbound, packet.Audit.Channel);
+ Assert.Equal(AuditKind.CachedSubmit, packet.Audit.Kind);
+ Assert.Equal(AuditStatus.Submitted, packet.Audit.Status);
+ Assert.Equal("myDb", packet.Audit.Target);
+ Assert.Equal(trackedId.Value, packet.Audit.CorrelationId);
+
+ Assert.Equal(trackedId, packet.Operational.TrackedOperationId);
+ Assert.Equal("DbOutbound", packet.Operational.Channel);
+ Assert.Equal("myDb", packet.Operational.Target);
+ Assert.Equal(SiteId, packet.Operational.SourceSite);
+ Assert.Equal("Submitted", packet.Operational.Status);
+ Assert.Equal(0, packet.Operational.RetryCount);
+ Assert.Null(packet.Operational.TerminalAtUtc);
+ }
+
+ [Fact]
+ public async Task CachedWrite_ProvenancePopulated()
+ {
+ var gateway = new Mock();
+ gateway
+ .Setup(g => g.CachedWriteAsync(
+ It.IsAny(), It.IsAny(),
+ It.IsAny?>(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Returns(Task.CompletedTask);
+ var forwarder = new CapturingForwarder();
+
+ var helper = CreateHelper(gateway.Object, forwarder);
+ await helper.CachedWrite("myDb", "INSERT INTO t VALUES (1)");
+
+ var packet = Assert.Single(forwarder.Telemetry);
+ Assert.Equal(SiteId, packet.Audit.SourceSiteId);
+ Assert.Equal(InstanceName, packet.Audit.SourceInstanceId);
+ Assert.Equal(SourceScript, packet.Audit.SourceScript);
+ Assert.Equal(SiteId, packet.Operational.SourceSite);
+ }
+
+ [Fact]
+ public async Task CachedWrite_ReturnsTrackedOperationId_ThreadsIdToGateway()
+ {
+ var gateway = new Mock();
+ gateway
+ .Setup(g => g.CachedWriteAsync(
+ It.IsAny(), It.IsAny(),
+ It.IsAny?>(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Returns(Task.CompletedTask);
+ var forwarder = new CapturingForwarder();
+
+ var helper = CreateHelper(gateway.Object, forwarder);
+ var trackedId = await helper.CachedWrite("myDb", "INSERT INTO t VALUES (1)");
+
+ Assert.NotEqual(default, trackedId);
+ gateway.Verify(g => g.CachedWriteAsync(
+ "myDb", "INSERT INTO t VALUES (1)",
+ It.IsAny?>(),
+ InstanceName,
+ It.IsAny(),
+ trackedId),
+ Times.Once);
+ }
+
+ [Fact]
+ public async Task CachedWrite_ForwarderThrows_StillReturnsTrackedOperationId()
+ {
+ var gateway = new Mock();
+ gateway
+ .Setup(g => g.CachedWriteAsync(
+ It.IsAny(), It.IsAny(),
+ It.IsAny?>(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Returns(Task.CompletedTask);
+ var forwarder = new CapturingForwarder
+ {
+ ThrowOnForward = new InvalidOperationException("simulated forwarder outage"),
+ };
+
+ var helper = CreateHelper(gateway.Object, forwarder);
+ var trackedId = await helper.CachedWrite("myDb", "INSERT INTO t VALUES (1)");
+
+ Assert.NotEqual(default, trackedId);
+ gateway.Verify(g => g.CachedWriteAsync(
+ "myDb", "INSERT INTO t VALUES (1)",
+ It.IsAny?>(),
+ InstanceName,
+ It.IsAny(),
+ trackedId),
+ Times.Once);
+ }
+}