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.
95 lines
4.3 KiB
Markdown
95 lines
4.3 KiB
Markdown
# 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 1188–1314).
|
||
|
||
The origin-recenter swap incorrectly treated every pending-only live bucket
|
||
as another landblock presentation generation (`GpuWorldState.cs`, former
|
||
lines 1352–1353). It therefore emitted a second full cleanup receipt for the
|
||
same already-retired generation. `LandblockRetirementCoordinator` correctly
|
||
rejected that duplicate at lines 416–425. 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.
|