diff --git a/SVSim.BattleNode/Reliability/InboundTracker.cs b/SVSim.BattleNode/Reliability/InboundTracker.cs
new file mode 100644
index 0000000..199e412
--- /dev/null
+++ b/SVSim.BattleNode/Reliability/InboundTracker.cs
@@ -0,0 +1,20 @@
+namespace SVSim.BattleNode.Reliability;
+
+///
+/// Per-session inbound-emit ledger. Dedupes the client's pubSeq so we never dispatch
+/// a retransmitted emit twice; ack-echo (via SIO callback) is the caller's job.
+///
+public sealed class InboundTracker
+{
+ private readonly HashSet _seen = new();
+
+ /// Highest pubSeq observed so far. Reported via Gungnir for diagnostics.
+ public long HighWaterMark { get; private set; }
+
+ /// Record an incoming pubSeq. Returns true if the caller should dispatch the envelope, false on duplicate.
+ public bool Observe(long pubSeq)
+ {
+ if (pubSeq > HighWaterMark) HighWaterMark = pubSeq;
+ return _seen.Add(pubSeq);
+ }
+}
diff --git a/SVSim.UnitTests/BattleNode/Reliability/InboundTrackerTests.cs b/SVSim.UnitTests/BattleNode/Reliability/InboundTrackerTests.cs
new file mode 100644
index 0000000..d811786
--- /dev/null
+++ b/SVSim.UnitTests/BattleNode/Reliability/InboundTrackerTests.cs
@@ -0,0 +1,41 @@
+using NUnit.Framework;
+using SVSim.BattleNode.Reliability;
+
+namespace SVSim.UnitTests.BattleNode.Reliability;
+
+[TestFixture]
+public class InboundTrackerTests
+{
+ [Test]
+ public void Observe_FirstSeenPubSeq_ReturnsDispatchTrue()
+ {
+ var t = new InboundTracker();
+ Assert.That(t.Observe(pubSeq: 1), Is.True);
+ }
+
+ [Test]
+ public void Observe_SamePubSeqTwice_SecondReturnsFalse()
+ {
+ var t = new InboundTracker();
+ t.Observe(1);
+ Assert.That(t.Observe(1), Is.False);
+ }
+
+ [Test]
+ public void Observe_DifferentPubSeqs_BothDispatch()
+ {
+ var t = new InboundTracker();
+ Assert.That(t.Observe(1), Is.True);
+ Assert.That(t.Observe(2), Is.True);
+ }
+
+ [Test]
+ public void HighWaterMark_TracksHighestObserved()
+ {
+ var t = new InboundTracker();
+ t.Observe(3);
+ t.Observe(1);
+ t.Observe(5);
+ Assert.That(t.HighWaterMark, Is.EqualTo(5));
+ }
+}