acdream/docs/research/2026-08-02-collision-throughput-handoff/p1-retirement-receipt-loop.md
Erik 01f4791e95 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.
2026-08-02 20:53:11 +02:00

95 lines
4.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# P1 — origin-recenter retirement-receipt loop
## Observed failure
`launch-feeltest-oclone.log` contains 243 consecutive failures with this
shape:
```text
streaming: origin-recenter preparation will resume:
InvalidOperationException: Landblock 0xC85AFFFF already has a full
retirement receipt.
```
The stack is `StreamingController.TryAdvanceOriginRecenterPreparation`
`LandblockPresentationPipeline.DetachAllForOriginRecenter`
`LandblockRetirementCoordinator.AdoptDetachedFull`.
## Root cause
An ordinary full retirement detaches every landblock-owned presentation
resource first, then parks surviving live entities in
`GpuWorldState._pendingByLandblock` while the exact cleanup ticket advances
asynchronously (`GpuWorldState.DetachLandblock`, around lines 11881314).
The origin-recenter swap incorrectly treated every pending-only live bucket
as another landblock presentation generation (`GpuWorldState.cs`, former
lines 13521353). It therefore emitted a second full cleanup receipt for the
same already-retired generation. `LandblockRetirementCoordinator` correctly
rejected that duplicate at lines 416425. Because spatial detachment had
already committed, the broad retry catch in
`StreamingController.TryAdvanceOriginRecenterPreparation` then repeated the
detach against the changed state every frame.
The pre-fix regression test
`OriginRecenterAdoption_PendingOnlyLiveProjectionDoesNotCreateSecondFullReceipt`
failed because the recenter returned one receipt for the pending-only bucket.
## Retail and reference boundary
Retail destroys one concrete landblock owner synchronously:
`CLandBlock::destroy_static_objects` (`0x0052FA50`) leaves and deletes the
landblock's static objects; `CLandBlock::Destroy` (`0x0052FAA0`) releases its
buildings and landblock data; `CLandBlock::release_all` (`0x0052FCF0`)
releases the landblock's object and visibility ownership. A live object
parked outside a loaded landblock is not a second `CLandBlock` and therefore
cannot create a second landblock-destruction transaction.
The extracted WorldBuilder reference follows the same ownership boundary:
`ObjectRenderManagerBase` removes an actual `_landblocks` entry before
`UnloadLandblockResources`, and `PortalRenderManager` only unloads a removed
`PortalLandblock`. Neither treats an independently parked object as a new
landblock resource owner.
Acdream retains its approved asynchronous adaptation: the first exact
receipt owns cleanup, while the live projection survives spatial recentering.
## Fix
- `GpuWorldState.DetachAllForOriginRecenter` no longer creates retirement
receipts from `_pendingByLandblock` alone. Pending live identities are
still captured from `_projectionLocations`, cleared atomically, and
re-parked unchanged.
- A landblock that also owns loaded, pending-render, pending-near, tier, or
bounds state still receives its exact full receipt.
- A receipt-ledger invariant thrown after spatial detachment is now surfaced
as a committed `StreamingMutationException`; it is terminal rather than
falsely logged as resumable work.
- The genuine duplicate-receipt guard remains unchanged.
## Deterministic evidence
- The new pending-only regression failed before the source fix and passes
afterward.
- `OriginRecenter_PendingOnlyLiveProjectionKeepsItsExistingRetirementOwner`
drives the production recenter/controller sequence and proves the origin
commits while the first cleanup ticket remains pending.
- `OriginRecenter_CommittedReceiptInvariantFailsFastInsteadOfReplayingDetach`
proves a genuine post-detach ledger violation surfaces once rather than
entering a frame-by-frame retry loop.
- The complete `OriginRecenter` focused group passes 20/20.
## Gate evidence
- Release build: 0 errors (21 pre-existing warnings).
- Complete Release suite: 10,815 passed, 0 failed, 4 skipped.
- Connected lifecycle/reconnect gate:
`logs/connected-world-gate-20260802-203751/report.json``Passed=true`.
- Connected nine-stop soak:
`logs/connected-r6-soak-20260802-204309.report.json``Passed=true`,
`Failures=[]`, graceful exit, all 9 canonical checkpoints present, no wait
cue, no pending landblock retirement, no reveal invariant failure, and no
render-shadow mismatch.
- The soak artifacts contain zero occurrences of
`already has a full retirement receipt`; the captured failing session had
243.