acdream/docs/research/2026-08-04-c4-route-4b-1-contract.md
Erik 9e97be1896 docs: pin the C4 route 4b-1 contract (remote placement infrastructure)
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>
2026-08-04 01:11:42 +02:00

174 lines
9 KiB
Markdown

# 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`](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
1. **A per-entity remote placement owner** in `AcDream.Runtime`.
2. **A Position-time service-window guard**, with a Runtime-facing interface and
implementations for both hosts.
3. **The refuse-rather-than-park policy** (see "The central decision").
4. **N3** — headless never calls `RetryPending` after construction.
5. **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`, the
`SendPositionEvent` plumbing — retail's remote arm has NO
`SendPositionEvent`. `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._driveScratch` is 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 Chebyshev
`dx <= 1 && dy <= 1` test against `_requestedCenterLandblock`. Its only
consumer today is Create-time.
- **The graphical host has none.** It must be built from
`GpuWorldState.IsNearTier` / `IsNearTierOrPending`, `StreamingController.NearRadius`
and 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.
- `ToCellessCreateRoute` does **not** apply. It produces `AwaitFreshPosition`,
its only consumer operates on a residence lease, and it refuses once
`FullCellId != 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.
- **`ConstrainTo` arming.** Retail has exactly ONE site on the remote arm
(@0x00454272); all three nonzero-returning `MoveOrTeleport` branches funnel
through it. Do not add a second. 4b-2/4b-3 fold the existing arm outward.
- **Route 1's executor.** `RuntimeAcceptedPositionRouteRequests` is shared with
it; any change there is a regression in shipped functionality.
## Contract
1. The owner is Runtime-resident and presentation-independent; both hosts can
drive it.
2. 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.
3. No ack. No re-issue funnel. Per-entity state.
4. A non-placeable destination is refused, not parked.
5. The ownership ledger includes the per-entity pending count and converges to
zero at teardown, reset, and generation change.
6. `ParkCollisionResidents`'s overlap throw is demonstrably unreachable.
7. 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** — `InWorld` true, 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.