diff --git a/ROADMAP.md b/ROADMAP.md
index afab51d..602f4b0 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -11,7 +11,7 @@ and ideas that are not ready for scheduling are recorded in
## Active milestone
-### Milestone 24.8 — MSI direct-UDP retrieval foundation
+### Milestone 24.8 — MSI UDP correlation/completion hardening
The first four Milestone 24.8 slices established lossless bounded MSI XML
modeling/parsing, exact serialized `MSI\r` retrieval for CR-line/replay
@@ -27,6 +27,15 @@ assembled; numbered `Footer`/`Foot` fragments use the existing bounded
reassembly path; retryable sequence failures resend the exact original `MSI`
wire; and successful completion clears one-shot MSI retry/expectation state.
+This sixth narrow hardening slice adds no production protocol behavior. It adds
+direct behavioral regression coverage for two invariants already provided by
+the shared UDP machinery: an exact one-shot MSI expectation survives unrelated
+bare XML roots until matching MSI XML arrives, and successful MSI completion
+after an automatic sequence-gap retry removes the one-shot retry authority so
+later stray MSI fragment gaps cannot resend a stale command. The tests use only
+deterministic fake datagrams and synchronize on a later ordinary response before
+asserting that no additional UDP write occurred.
+
Direct `UdpTransport` and its capture wrapper are covered only with deterministic
fake datagrams. This is software transport/framing evidence, not a physical
scanner or firmware-support claim. Fallback transports remain blocked because
@@ -35,7 +44,7 @@ has been established. Custom transports that merely advertise an `udp://`
endpoint remain fail-closed unless they are the repository's direct
`UdpTransport` path.
-This slice does not add `MSV`/`MSB` execution, unindexed `MNU`, menu
+This hardening slice does not add `MSV`/`MSB` execution, unindexed `MNU`, menu
lifecycle/state ownership, renderer behavior, model/firmware applicability, or
physical validation. The serialized `[RSV]` field in `MSV`/`MSB` remains
unresolved, and Quick Search (`QSH`) remains blocked pending exact `FRQ`
diff --git a/docs/advanced-protocol-research.md b/docs/advanced-protocol-research.md
index 6784db1..a2b11b2 100644
--- a/docs/advanced-protocol-research.md
+++ b/docs/advanced-protocol-research.md
@@ -588,7 +588,18 @@ completion clears MSI one-shot retry state. `SDSScanner.get_msi()` is therefore
allowed on the repository's direct `UdpTransport`, including when wrapped by
capture recording.
-All UDP evidence in this slice is deterministic fake-datagram software evidence.
+The sixth narrow slice adds no production protocol behavior. It hardens the
+existing direct-UDP contract with explicit behavioral regressions for root
+correlation and one-shot retry cleanup: while `MSI` is expected, unrelated bare
+`ScannerInfo`, `GLT`, or `AST` XML remains uncorrelated and does not consume the
+MSI expectation; after a retryable MSI fragment gap and a subsequent successful
+MSI completion, another stray MSI fragment gap cannot reuse the completed
+request's automatic-retry authority. The latter assertion waits for a later
+ordinary decoded response before checking the transmitted datagrams, avoiding a
+negative-write race.
+
+All UDP evidence in the fifth and sixth slices is deterministic fake-datagram
+software evidence.
It does not establish physical SDS200 behavior, firmware availability, or a
broader transport guarantee. Fallback transports remain fail-closed because
their active transport can change and no fallback-wide MSI framing contract is
diff --git a/tests/test_network.py b/tests/test_network.py
index fdf25e4..b436c29 100644
--- a/tests/test_network.py
+++ b/tests/test_network.py
@@ -284,6 +284,29 @@ def test_udp_decoder_does_not_expect_nonexact_msi_command(command: str) -> None:
assert decoder.feed(bare_xml) == (bare_xml.decode(),)
+@pytest.mark.parametrize(
+ "other_xml",
+ [
+ (
+ b''
+ b""
+ ),
+ b'',
+ b'',
+ ],
+)
+def test_udp_decoder_keeps_msi_expectation_across_other_xml_roots(
+ other_xml: bytes,
+) -> None:
+ decoder = UdpDatagramDecoder()
+ decoder.expect_command("MSI")
+ msi = b''
+
+ assert decoder.feed(other_xml) == (other_xml.decode(),)
+ assert decoder.feed(msi) == ("MSI,,", msi.decode())
+ assert decoder.feed(msi) == (msi.decode(),)
+
+
def test_decoder_malformed_bare_glt_does_not_complete_expectation() -> None:
completed: list[str] = []
decoder = UdpDatagramDecoder(completion_handler=completed.append)
@@ -504,6 +527,61 @@ def test_udp_transport_retries_msi_with_exact_original_wire_command() -> None:
assert transport.statistics["xml_fragments_dropped"] == 1
+def test_udp_transport_completed_msi_clears_one_shot_retry_state() -> None:
+ fake = FakeDatagramSocket()
+ transport = UdpTransport(
+ "192.0.2.25",
+ socket_factory=FakeDatagramSocketFactory(fake),
+ reconnect=False,
+ max_xml_retries=2,
+ )
+ diagnostics: list[TransportDiagnostic] = []
+ received: list[str] = []
+ transport.set_diagnostic_handler(diagnostics.append)
+ transport.start(received.append)
+ try:
+ transport.write_command("MSI")
+ fake.feed(
+ b'MSI,,'
+ b''
+ )
+ fake.feed(
+ b'MSI,,'
+ b''
+ )
+ wait_until(lambda: fake.sent == [b"MSI\r", b"MSI\r"])
+
+ fake.feed(
+ b'MSI,,'
+ )
+ wait_until(
+ lambda: transport.statistics["xml_documents_completed"] == 1
+ )
+
+ fake.feed(
+ b'MSI,,'
+ b''
+ )
+ fake.feed(
+ b'MSI,,'
+ b''
+ )
+ fake.feed(b"MDL,SDS200\r")
+ wait_until(lambda: "MDL,SDS200" in received)
+ finally:
+ transport.stop()
+
+ assert [diagnostic.kind for diagnostic in diagnostics] == [
+ "sequence_gap",
+ "sequence_gap",
+ ]
+ assert fake.sent == [b"MSI\r", b"MSI\r"]
+ assert transport.statistics["commands_sent"] == 2
+ assert transport.statistics["retries_sent"] == 1
+ assert transport.statistics["xml_fragments_dropped"] == 2
+ assert transport.statistics["xml_documents_completed"] == 1
+
+
def test_radio_network_parses_bare_glt_favorites() -> None:
fake = FakeDatagramSocket()
radio = SDS200.network(