fix(streaming): stop replaying committed recenter retirements

Root cause: pending-only live projection buckets were misclassified as landblock presentation owners during origin recentering. That manufactured a second full cleanup receipt for a generation whose first receipt was still advancing; the duplicate guard threw and the broad retry path replayed the already-committed detach 243 times.

Keep pending live projections through the spatial identity map without issuing another receipt, and fail fast when a receipt-ledger invariant occurs after detachment. Evidence: docs/research/2026-08-02-collision-throughput-handoff/p1-retirement-receipt-loop.md. Release suite, lifecycle gate, and nine-stop soak pass.
This commit is contained in:
Erik 2026-08-02 20:53:11 +02:00
parent c65559d8f8
commit 01f4791e95
9 changed files with 309 additions and 9 deletions

View file

@ -595,6 +595,34 @@ public sealed class LandblockRetirementCoordinatorTests
Assert.Equal(0, detachedCallbacks);
}
[Fact]
public void OriginRecenterAdoption_PendingOnlyLiveProjectionDoesNotCreateSecondFullReceipt()
{
const uint landblockId = 0x4648FFFFu;
WorldEntity live = Entity(1, serverGuid: 0x70000003u);
GpuWorldState state = StateWith(landblockId, live);
LandblockRetirementCoordinator coordinator =
LandblockRetirementCoordinator.CreateBudgeted(
state,
ticket => AdvancePresentationStep(ticket),
ticket => CompletePresentation(ticket));
// The ordinary retirement has already detached every landblock-owned
// resource. The still-live entity is parked in the pending bucket
// while that exact cleanup receipt advances asynchronously.
coordinator.BeginFull(landblockId);
Assert.Equal(1, coordinator.PendingCount);
Assert.False(state.IsLoaded(landblockId));
GpuWorldRecenterRetirement recenter =
state.DetachAllForOriginRecenter();
Assert.Empty(recenter.Landblocks);
Assert.Null(coordinator.AdoptDetachedFull(recenter.Landblocks));
Assert.Equal(1, coordinator.PendingCount);
Assert.True(coordinator.IsPending(landblockId));
}
[Fact]
public void OriginRecenterAdoption_ObserverFailureRetainsEveryCleanupReceipt()
{