diff --git a/src/ScadaLink.AuditLog/Site/Telemetry/CachedCallLifecycleBridge.cs b/src/ScadaLink.AuditLog/Site/Telemetry/CachedCallLifecycleBridge.cs
index 121370f..8ca8ba4 100644
--- a/src/ScadaLink.AuditLog/Site/Telemetry/CachedCallLifecycleBridge.cs
+++ b/src/ScadaLink.AuditLog/Site/Telemetry/CachedCallLifecycleBridge.cs
@@ -162,6 +162,9 @@ public sealed class CachedCallLifecycleBridge : ICachedCallLifecycleObserver
Channel: context.Channel,
Target: context.Target,
SourceSite: context.SourceSite,
+ // SourceNode: stamped by Task 14 once the bridge gets an
+ // INodeIdentityProvider; null until then.
+ SourceNode: null,
Status: operationalStatus,
RetryCount: context.RetryCount,
LastError: lastError,
diff --git a/src/ScadaLink.Commons/Entities/Audit/SiteCall.cs b/src/ScadaLink.Commons/Entities/Audit/SiteCall.cs
index f83f3ab..96c807e 100644
--- a/src/ScadaLink.Commons/Entities/Audit/SiteCall.cs
+++ b/src/ScadaLink.Commons/Entities/Audit/SiteCall.cs
@@ -30,6 +30,15 @@ public sealed record SiteCall
/// Site id that submitted the cached call.
public required string SourceSite { get; init; }
+ ///
+ /// The cluster node on which the cached call was emitted — node-a /
+ /// node-b for site rows (qualified by ),
+ /// central-a / central-b for central-originated rows. Stamped
+ /// by the emitting node from INodeIdentityProvider; nullable so
+ /// reconciled rows from a node that has since been retired don't block ingest.
+ ///
+ public string? SourceNode { get; init; }
+
///
/// Lifecycle status — string form of
/// . Monotonic: later rank
diff --git a/src/ScadaLink.Commons/Types/SiteCallOperational.cs b/src/ScadaLink.Commons/Types/SiteCallOperational.cs
index 25d9414..40acf6a 100644
--- a/src/ScadaLink.Commons/Types/SiteCallOperational.cs
+++ b/src/ScadaLink.Commons/Types/SiteCallOperational.cs
@@ -21,6 +21,13 @@ namespace ScadaLink.Commons.Types;
///
/// Human-readable target (e.g. "ERP.GetOrder").
/// Site id that submitted the cached call.
+///
+/// The cluster node on which the cached call was emitted — node-a / node-b
+/// for site rows (qualified by ), central-a /
+/// central-b for central-originated rows. Stamped by the emitting node from
+/// INodeIdentityProvider; nullable so reconciled rows from a node that has since
+/// been retired don't block ingest.
+///
///
/// Lifecycle status — string form of :
/// Submitted, Retrying, Attempted, Delivered,
@@ -37,6 +44,7 @@ public sealed record SiteCallOperational(
string Channel,
string Target,
string SourceSite,
+ string? SourceNode,
string Status,
int RetryCount,
string? LastError,
diff --git a/src/ScadaLink.SiteRuntime/Scripts/ScriptRuntimeContext.cs b/src/ScadaLink.SiteRuntime/Scripts/ScriptRuntimeContext.cs
index a8c68fb..8f8314a 100644
--- a/src/ScadaLink.SiteRuntime/Scripts/ScriptRuntimeContext.cs
+++ b/src/ScadaLink.SiteRuntime/Scripts/ScriptRuntimeContext.cs
@@ -648,6 +648,9 @@ public class ScriptRuntimeContext
Channel: "ApiOutbound",
Target: target,
SourceSite: _siteId,
+ // SourceNode: stamped by Task 14 once the script context
+ // gets an INodeIdentityProvider; null until then.
+ SourceNode: null,
Status: "Submitted",
RetryCount: 0,
LastError: null,
@@ -766,6 +769,9 @@ public class ScriptRuntimeContext
Channel: "ApiOutbound",
Target: target,
SourceSite: _siteId,
+ // SourceNode: stamped by Task 14 once the script context
+ // gets an INodeIdentityProvider; null until then.
+ SourceNode: null,
Status: "Attempted",
// RetryCount stays 0 — the operation never reached the
// S&F retry sweep, so no retries were performed.
@@ -833,6 +839,9 @@ public class ScriptRuntimeContext
Channel: "ApiOutbound",
Target: target,
SourceSite: _siteId,
+ // SourceNode: stamped by Task 14 once the script context
+ // gets an INodeIdentityProvider; null until then.
+ SourceNode: null,
Status: operationalTerminalStatus,
RetryCount: 0,
LastError: result.Success ? null : result.ErrorMessage,
@@ -1243,6 +1252,9 @@ public class ScriptRuntimeContext
Channel: "DbOutbound",
Target: target,
SourceSite: _siteId,
+ // SourceNode: stamped by Task 14 once the script context
+ // gets an INodeIdentityProvider; null until then.
+ SourceNode: null,
Status: "Submitted",
RetryCount: 0,
LastError: null,
diff --git a/tests/ScadaLink.AuditLog.Tests/Integration/CachedCallCombinedTelemetryTests.cs b/tests/ScadaLink.AuditLog.Tests/Integration/CachedCallCombinedTelemetryTests.cs
index 342b028..58e1ce0 100644
--- a/tests/ScadaLink.AuditLog.Tests/Integration/CachedCallCombinedTelemetryTests.cs
+++ b/tests/ScadaLink.AuditLog.Tests/Integration/CachedCallCombinedTelemetryTests.cs
@@ -72,6 +72,7 @@ public class CachedCallCombinedTelemetryTests : TestKit, IClassFixture
+/// Verifies the central operational entity carries the
+/// SourceNode column (additive, nullable) through init-only construction and
+/// with expressions. Sibling to .
+///
+public class SiteCallTests
+{
+ private static SiteCall MinimalRow() => new()
+ {
+ TrackedOperationId = TrackedOperationId.New(),
+ Channel = "ApiOutbound",
+ Target = "ERP.GetOrder",
+ SourceSite = "site-01",
+ Status = "Submitted",
+ RetryCount = 0,
+ CreatedAtUtc = new DateTime(2026, 5, 23, 12, 0, 0, DateTimeKind.Utc),
+ UpdatedAtUtc = new DateTime(2026, 5, 23, 12, 0, 0, DateTimeKind.Utc),
+ IngestedAtUtc = new DateTime(2026, 5, 23, 12, 0, 1, DateTimeKind.Utc),
+ };
+
+ [Fact]
+ public void SiteCall_carries_SourceNode()
+ {
+ // SourceNode identifies the cluster node that emitted the cached call
+ // (site node-a/node-b or central-a/central-b). Additive nullable init
+ // property — defaults to null on rows ingested before the column
+ // existed, and round-trips its value via `with` expressions.
+ var row = MinimalRow();
+ Assert.Null(row.SourceNode);
+
+ var stamped = row with { SourceNode = "node-b" };
+ Assert.Equal("node-b", stamped.SourceNode);
+ Assert.Null(row.SourceNode);
+ }
+}
diff --git a/tests/ScadaLink.Commons.Tests/Messages/Integration/CachedCallTelemetryTests.cs b/tests/ScadaLink.Commons.Tests/Messages/Integration/CachedCallTelemetryTests.cs
index 03980d8..5c1bca7 100644
--- a/tests/ScadaLink.Commons.Tests/Messages/Integration/CachedCallTelemetryTests.cs
+++ b/tests/ScadaLink.Commons.Tests/Messages/Integration/CachedCallTelemetryTests.cs
@@ -60,6 +60,10 @@ public class CachedCallTelemetryTests
Channel: nameof(AuditChannel.ApiOutbound),
Target: "ERP.GetOrder",
SourceSite: SiteId,
+ // SourceNode: actual stamping arrives with Task 14; for now the
+ // packet builder leaves the column null so existing assertions on
+ // the packet's other fields stay intact.
+ SourceNode: null,
Status: status.ToString(),
RetryCount: retryCount,
LastError: lastError,
diff --git a/tests/ScadaLink.Commons.Tests/Types/SiteCallOperationalTests.cs b/tests/ScadaLink.Commons.Tests/Types/SiteCallOperationalTests.cs
new file mode 100644
index 0000000..29d9eb4
--- /dev/null
+++ b/tests/ScadaLink.Commons.Tests/Types/SiteCallOperationalTests.cs
@@ -0,0 +1,42 @@
+using ScadaLink.Commons.Types;
+
+namespace ScadaLink.Commons.Tests.Types;
+
+///
+/// Verifies — the positional record carried on
+/// the combined CachedCallTelemetry packet — round-trips the SourceNode
+/// field through positional construction (where the parameter sits between
+/// SourceSite and Status, mirroring the central SiteCalls
+/// table column order).
+///
+public class SiteCallOperationalTests
+{
+ [Fact]
+ public void SiteCallOperational_carries_SourceNode()
+ {
+ // SourceNode identifies the cluster node that emitted the cached call
+ // (site node-a/node-b or central-a/central-b). Nullable — callsites
+ // pass null until INodeIdentityProvider stamping arrives in Task 14.
+ var trackedId = TrackedOperationId.New();
+ var nowUtc = new DateTime(2026, 5, 23, 12, 0, 0, DateTimeKind.Utc);
+
+ var defaulted = new SiteCallOperational(
+ TrackedOperationId: trackedId,
+ Channel: "ApiOutbound",
+ Target: "ERP.GetOrder",
+ SourceSite: "site-01",
+ SourceNode: null,
+ Status: "Submitted",
+ RetryCount: 0,
+ LastError: null,
+ HttpStatus: null,
+ CreatedAtUtc: nowUtc,
+ UpdatedAtUtc: nowUtc,
+ TerminalAtUtc: null);
+ Assert.Null(defaulted.SourceNode);
+
+ var stamped = defaulted with { SourceNode = "node-a" };
+ Assert.Equal("node-a", stamped.SourceNode);
+ Assert.Null(defaulted.SourceNode);
+ }
+}