4b-1 builds the machinery and changes no remote behaviour: a per-entity remote placement owner, a Position-time service-window guard on both hosts, the refuse-rather-than-park policy, N3's headless RetryPending pump, and parked-count observability. No production caller, so 4b-2 and 4b-3 flip it on afterwards. It lands alone because it is where the park-withdraws-the-entity failure mode is decided, and that decision needs its own review signal rather than sharing one with a ~700-line class deletion. Pins the central decision: refuse rather than park. A DeferredCell park withdraws the entity (InWorld false, Active cleared, clock suspended, residency dropped), and Forget-on-every-accepted-Position kills the park without restoring any of it — so a remote that parks and is then superseded by an Interpolate packet stays withdrawn indefinitely, invisible and intangible. Names the two transfer errors that would look correct to anyone copying route 2's controller: do not port the ack machinery (retail's remote arm has no SendPositionEvent) and do not port the re-issue funnel (re-issuing a superseded pose is wrong for a repeated 5-10 Hz stream). Makes ParkCollisionResidents' overlap throw a gate item rather than a note — it is unreachable today only because steady-state remotes hold no operations, and with N remotes an ordinary streaming retirement would become session-fatal. Lists what 4b-1 must not touch, including AP-135's two writes (4a-owned dispositions that sit inside the method 4b rewrites — the trap) and the single retail ConstrainTo arming site. Records both known flakes by number and mechanism so they cannot be conflated again: #302 is a GC-allocation assertion in App.Tests, #308 a wall-clock deadline in Core.Net.Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9 KiB
C4 route 4b-1 — remote placement infrastructure: pinned contract (2026-08-04)
Scoping and the three-way split:
2026-08-04-c4-route-4b-scoping-and-split.md.
4b-1 builds the machinery and changes NO remote behaviour. It has no production caller, or is called for zero classifications. 4b-2 (far branch) and 4b-3 (teleport / cell-less) flip it on afterwards.
The reason for landing it alone: 4b-1 is where the park-withdraws-the-entity failure mode is decided, and that decision must be reviewed on its own signal, not alongside a ~700-line class deletion.
Scope — build these five things
- A per-entity remote placement owner in
AcDream.Runtime. - A Position-time service-window guard, with a Runtime-facing interface and implementations for both hosts.
- The refuse-rather-than-park policy (see "The central decision").
- N3 — headless never calls
RetryPendingafter construction. - Parked-count observability wired into the ownership ledger and
report.json.
The central decision — refuse, do not park
A Runtime DeferredCell park withdraws the entity from the world:
ParkDeferred sets body.InWorld = false, clears TransientStateFlags.Active,
suspends the object clock, calls WithdrawCanonical, and publishes a Withdraw.
RuntimeEntityObjectLifetime's Forget-on-every-accepted-Position then kills
the park without restoring any of that — CancelCoreDeferred removes the
operation and rewrites the Withdraw into a Discard; it does not set
InWorld back, resume the clock, or re-enter residency.
So packet N parks entity E (invisible AND intangible); packet N+1 ~150 ms later
Forgets the park; and if N+1 classifies Interpolate, no placement runs and E
stays withdrawn indefinitely. The producing sequence is mundane: a remote
appears beyond 96 m, walks toward you, crosses inside 96 m.
Therefore: a remote whose destination is not placeable now must NOT open a park. It keeps its last committed pose and waits. The next packet IS the retry, because remote Positions are a 5-10 Hz stream. This is also retail-shaped — retail's world is fully resident, so "arrived but not placeable" is unrepresentable there.
If you conclude the refusal cannot be expressed without touching
RuntimeSetPositionState's park machinery, STOP and report rather than adding a
withdrawal-restore path inside a 5,652-line class.
Two transfer errors — named so they are not repeated
Route 2's controller is the architectural model. Two of its parts must NOT be ported, and both would look correct to an implementer copying it:
- The ack machinery.
PositionEventOwed,positionEventOwed, theSendPositionEventplumbing — retail's remote arm has NOSendPositionEvent.HandleReceivedPosition@0x00453FD0 calls it only on the local-player FORCE_POSITION branch. Delete the concept, do not carry it. - The re-issue funnel. Route 2 re-issues because a ForcePosition is a one-shot correction ACE never repeats, so a dropped one is lost. A remote Position is a repeated stream; re-issuing packet N after N+1 has merged would apply a pose the newer packet already superseded. Same class of error as the route-2-to-4a rebucket mistake, in reverse.
Per-entity state
Route 2's _pending is ONE slot and RetainPending throws on a second live
pending. That shape does not transfer. Required:
- A per-key map, with route 2's single-owner invariant re-derived per entity.
- The ownership ledger count becomes a dictionary count that must converge to zero at teardown, reset, and generation change.
- Bounded, non-allocating iteration for any pump —
RuntimeFirstEntryDriveController._driveScratchis the in-repo template. - Per-entity currency: GUID reuse, incarnation change, generation change, an entity torn down mid-drain, and two entities interleaved must all be safe. A pure function does not by itself make the CALLER re-validate identity.
The service-window guard
- Headless has one:
IHeadlessCollisionNeighborhood.IsWithinServiceWindow(HeadlessSessionWorldProjection.cs:27, impl:245-257) — a Chebyshevdx <= 1 && dy <= 1test against_requestedCenterLandblock. Its only consumer today is Create-time. - The graphical host has none. It must be built from
GpuWorldState.IsNearTier/IsNearTierOrPending,StreamingController.NearRadiusand the observer centre, exposed to Runtime through an interface mirroring the headless one. - Unproven, and you must establish it: that near-tier residency is exactly co-extensive with collision publication. The four gating call sites are consistent with it; the retirement side is unverified. If it is not co-extensive, say so and propose the correct predicate rather than shipping the assumption.
ToCellessCreateRoutedoes not apply. It producesAwaitFreshPosition, its only consumer operates on a residence lease, and it refuses onceFullCellId != 0. A steady-state Position has no residence by construction. 4b-1 needs its own "decline this placement" outcome.
N3 — fix regardless
HeadlessSessionEventRoute.Attach constructs the subscription with
retryPendingOnSubscribe: true and that is the ONLY RetryPending call headless
ever makes. PumpFirstEntry calls IsReady, DriveAll and Advance — not
RetryPending. The graphical host binds it correctly per frame.
RuntimePlacementProjectionSubscription.OnPlacement only projects when the new
delta IS the FIFO head, so a declined head is never revisited.
Add RetryPending() to the headless pump in the same order the graphical route
uses (drives first, retry last). This is a latent defect in shipped code; fix it
whether or not 4b-1 flips headless remotes.
Must prove, not assume
ParkCollisionResidents throws on overlap — for every spatial root in a
retiring landblock prefix that holds an active operation
(RuntimeSetPositionState.cs:3387-3395). With N remotes holding operations, an
ordinary streaming retirement becomes session-fatal. It is unreachable today
only because steady-state remotes hold no operations. 4b-1 must demonstrate it
stays unreachable under the new design — that is the concrete failure #277's
bound was protecting, and it is a gate item, not a note.
Do NOT touch
- AP-135's two writes (
rmState.CellId,LastServerPos/LastServerPosTime) on the airborne branches. They are 4a-owned dispositions and AP-135 does not retire with 4b. They sit inside the method 4b rewrites, which is the trap. RemoteTeleportController,RemoteTeleportPlacement,remotePlacementRequired— 4b-3.- The legacy far halves in either arm — 4b-2.
ConstrainToarming. Retail has exactly ONE site on the remote arm (@0x00454272); all three nonzero-returningMoveOrTeleportbranches funnel through it. Do not add a second. 4b-2/4b-3 fold the existing arm outward.- Route 1's executor.
RuntimeAcceptedPositionRouteRequestsis shared with it; any change there is a regression in shipped functionality.
Contract
- The owner is Runtime-resident and presentation-independent; both hosts can drive it.
- It has no production caller, or is called for zero classifications. 4b-1 changes no remote behaviour, and the existing connected routes must be unchanged.
- No ack. No re-issue funnel. Per-entity state.
- A non-placeable destination is refused, not parked.
- The ownership ledger includes the per-entity pending count and converges to zero at teardown, reset, and generation change.
ParkCollisionResidents's overlap throw is demonstrably unreachable.- Nothing in the "Do NOT touch" list changes.
Acceptance
- Focused Runtime tests: per-entity independence, currency across GUID reuse / incarnation / generation / teardown, the refusal outcome, ledger convergence, and the service-window predicate on both hosts.
- A test for the §"central decision" sequence specifically: attempt a
placement whose destination is not placeable, deliver a second accepted
Position that classifies
Interpolate, and assert the entity is still in the world —InWorldtrue, clock running, residency intact. That is the invisible-and-intangible failure, and it must be pinned before 4b-2 flips anything on. - Complete Release suite green. Baseline 10,938 passed / 4 skipped / 0
failed. Two known flakes, do NOT chase and do NOT conflate: #302
(
PortalProjectionTests.ClipToRegion_FrameOwnedStore_ReusesExactResultArray, a GC-allocation assertion in App.Tests) and #308 (NakEmissionTests.LossSoak_..., a wall-clock deadline in Core.Net.Tests that fails only under full-suite load). If either appears, re-run and say which. - No connected gate: 4b-1 changes no behaviour, so there is nothing for a user to observe. Say so rather than inventing one.
Budget
~700-1,000 non-comment production lines. Route 4a came in at 364, which was 91% of its ~400 budget — not "well under". If 4b-1 exceeds 1,000, stop and report before continuing rather than pushing through.