fix(physics): C4 route 3 — portal placement authority (local player)

Removes a duplicate placement authority for local-player portal arrival.
Portalling worked before this change and works after it — this is not a
bug fix, EXCEPT that it found and fixed one dead-code production bug.

THE PRODUCTION BUG: TryExecuteCanonicalPortalPlacement re-read the
accepted destination at Place time, but TryBeginPortalReveal already
consumes that slot at Aim time — so the arm was 100% dead code and every
real portal Place refused with host-token-unavailable. Found only
because we refused to accept 7 skipped tests instead of chasing the
count to zero.

RETAIL IS THE GENERIC PATH FOR THE THIRD ROUTE RUNNING:
SmartBox::TeleportPlayer @0x00453910 = SetPositionSimple(dest, 1) with
flags 0x1012, followed by PlayerPositionUpdated.

BOTH INVERSIONS, WITH THEIR ANCHORS: unlike route 2, the leash IS armed
here (ConstrainTo @0x0045418A) and velocity is zeroed
(set_velocity @0x004541B4); unlike route 4b-3, the local teleport_hook
runs AFTER placement (@0x004538AE).

THE THREE-ROUND DEFECT CHAIN, HONESTLY:
- Round 1 released the player at the pre-teleport position while the
  anim stream marched on — the contract wrongly assumed Place re-fires
  (process rule 1's third occurrence this campaign).
- Round 2's fix inferred commit from a global PendingCount, which three
  non-committing paths also clear — making the SAME bug complete
  cleanly and silently. Strictly worse than round 1: round 1 at least
  tripped portal-complete-before-materialized.
- Round 3 latches the commit where it actually happens
  (ReconcileAndAcknowledgePortal), keyed on reveal generation and
  teleport sequence, via TryConsumePortalCommit. Two of the three
  required regression tests landed and are sabotage-verified on both
  hosts (ParkedPlace_ForgottenByOrdinaryMergeDoesNotLatchAsCommitted /
  HeadlessPortalPrepareDestinationForgottenByOrdinaryMergeDoesNotLatchAsCommitted).
  The third (force-arm-takes-the-slot) was judged unnecessary on review:
  with the inference gone, PendingCount is only a "don't ask yet" guard
  at both gates, so a force operation occupying or vacating the slot no
  longer changes an input the commit decision reads — the case collapses
  into what the landed test already discriminates.

THE B2/P3 RESOLUTION: both round-2 reviews were right about different
branches of the same synchronous call. RuntimePlacementProjectionSubscription
.OnPlacement acknowledges the FIFO head only when TryApply returns true;
a Place whose portal authority went stale (transit ended/superseded
while parked) used to return false, wedging every later entity's
placement receipt behind it forever. Both sinks
(RuntimePlacementPresentationSink, HeadlessRuntimePlacementProjectionSink)
now acknowledge-and-ignore a stale-authority Place instead of refusing
it. The regression test (RuntimePlacementPresentationSinkTests
.PortalPlace_StaleTransitHostOrSequenceIsAcknowledgedAndIgnored) had
been asserting the old, wrong `false` behaviour; it now asserts and
sabotage-verifies the fix.

Also lands: AP-144 (register discipline — the portal movement-event
send reuses the stricter UsePositionFromServer gate where retail's
SendMovementEvent is the looser autonomy_level != 0 test, diverging
only at level 1, currently unreachable), AP-145 + issue #318 (the
local-player collision-shadow presentation write bypasses its own
publisher's ShadowObjects write via a direct cache .Set(), self-healing
only once dedup diverges — filed, not fixed, pending a composition
test), AD-42 deleted (its last citation retired by the canonical portal
arm), AD-2 updated (the wait-cue's trigger predicate now covers a
second cause), and two documentation corrections: the enter_world
misattribution (both call sites are in SmartBox::HandleCreateObject,
only one in the player branch — portal arrival is TeleportPlayer, not
enter_world) and the stale "local player never reaches this path"
comment on the generic-remote-render-pose write.

Suite: 11,090 passed / 4 skipped / 0 failed. No new skips, nothing
weakened.

STILL OWED: the connected two-client gate, with
ACDREAM_PROBE_LOCAL_TELEPORT=1, scored only if [local-tp] lines
actually appear in the capture — and explicitly NOT scored as covering
issue #318 (no composition test yet asserts PhysicsEngine.ShadowObjects
directly).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-05 03:57:37 +02:00
parent cd3129e9d6
commit e0f96a55bf
24 changed files with 5261 additions and 243 deletions

View file

@ -24,6 +24,65 @@ What does NOT go here:
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
## #318 — C4 route 3 §8 items 8/9/10 residual: no end-to-end composition test, no local-player shadow assertion, no T8 ordering
**Status:** OPEN
**Severity:** LOW (does not block round-3 acceptance per both reviewers; carried
into C5)
**Filed:** 2026-08-05, C4 route 3 round-3 review (retail B5/A5, architecture
B5), carried per both reviewers' explicit conditions
**Component:** Runtime / portal placement / local-player presentation
**Description:** The retail review's round-1 §3.4 premise — that
`TryApplyRuntimePlacementPlace` does not write pose/rotation/`ParentCellId` or
rebucket — was WRONG; round 3 verified it DOES. That closed the original
blocking concern, but three narrower gaps remain and both reviewers agreed
they must be tracked rather than silently dropped:
1. No end-to-end composition test exercises the full portal-arrival →
canonical commit → presentation-suffix → `PhysicsEngine.ShadowObjects`
chain for the LOCAL player specifically (existing tests cover pieces —
the canonical commit, the presentation sink's `TryApply`, the drive
controller — but not the full composed path with a real
`RuntimePlacementPresentationSink` wired to a real `PhysicsEngine`).
2. No test asserts the local-player collision SHADOW lands at the
destination. The discriminating assertion for that future test:
`PhysicsEngine.ShadowObjects` must hold a row at the destination cell/
position, not just `LocalPlayerShadowState`'s internal dedup cache — see
the register row (AP-131 amendment, filed alongside this issue) for the
asymmetry this exposes: `LocalPlayerShadowState.Set` updates the dedup
cache without publishing to `ShadowObjects`, self-healing only on the
local player's first subsequent movement tick.
3. No test proves T8's ordering — that the canonical commit's writes
(pose/rotation/`ParentCellId`/rebucket) precede the presentation suffix's
OWN redundant writes to the same fields, rather than racing or reversing.
**Root cause / status:** Not a defect — a coverage gap. The underlying
mechanism (`RuntimePlacementPresentationSink.TryApply`
`LiveEntityRuntime.TryApplyRuntimePlacementProjection`
`TryPublishPlace``LocalPlayerShadowState.Set`) is correct by code reading
and by the individual unit tests that DO exist; what's missing is the
COMPOSED, end-to-end proof plus the specific shadow-registry assertion.
**Files:** `src/AcDream.App/World/RuntimePlacementPresentationSink.cs`
(`TryPublishPlace`, `LocalPlayerShadowState.Set` call); `src/AcDream.App/Physics/LocalPlayerShadowState.cs`;
`src/AcDream.Core/Physics/PhysicsEngine.cs` (`ShadowObjects`);
`src/AcDream.Runtime/Session/RuntimeAcceptedPositionDriveController.cs`
(`ReconcileAndAcknowledgePortal`, the T8 probe log).
**Research:** `docs/research/2026-08-04-c4-route-3-contract.md` §3.4;
`docs/research/2026-08-04-c4-route-3-retail-review-round2.md` §D (B5/A5);
`docs/research/2026-08-04-c4-route-3-architecture-review-round2.md` B5.
**Acceptance:** A composition test drives a real portal arrival through the
canonical drive controller and the real `RuntimePlacementPresentationSink`
against a real `PhysicsEngine`, then asserts `PhysicsEngine.ShadowObjects`
holds the local player at the destination position/cell (not merely
`LocalPlayerShadowState`'s cache) and that the write ordering matches T8 (a
probe or log-order assertion). Do not score the existing connected/manual
gate as covering this — it exercises the live path but does not assert the
shadow registry specifically.
## #317`TryCommitAuthoritativeVelocity`'s call site has no established retail basis
**Status:** OPEN

File diff suppressed because one or more lines are too long

View file

@ -91,6 +91,34 @@ same commit) → docs/handoff commit. No workarounds; no fused slices.
and local-player owners.
- `RuntimePortalPlacementAuthority` has zero producing call sites; the
adapter from `RuntimeWorldTransitState` does not exist.
**Corrected 2026-08-04 (C4 route 3 closure,
`docs/research/2026-08-04-c4-route-3-contract.md`), itself corrected
2026-08-05 (A10 architecture review — the first correction asserted a
false fact of its own), and rewritten 2026-08-05 (N5 retail-review
round-3 fix — the prior wording of this correction contradicted
itself).** The original bullet conflated two separate claims into one
sentence, and only one of them was true. What pre-dated route 3 and WAS
accurate: the `RuntimePortalPlacementAuthority` type existed (referenced
by route 2's `Pending.Portal` field, always `Present: false`), its
`IsValid` check existed, and the sinks' portal-authority gates plus
`BeginAcceptedPlacementCore`'s gate already read it. What was NOT
accurate, and is what "zero producing call sites; the adapter does not
exist" actually described: the PRODUCER half — nothing built a
`Present: true` authority and called the consumer arm
(`RuntimeAcceptedPositionDriveController.TryExecuteAcceptedPortalArrival`/
`SubmitAndResolvePortal`/`ClassifyPortalArrival`) — that consumer arm
ALSO did not exist before route 3. Route 3 added the producer and the
consumer together, in the same slice: the producer is
`LocalPlayerTeleportController.TryExecuteCanonicalPortalPlacement` (now
`TryAdvancePortalCommit`/`TryExecuteCanonicalPortalPlacementCore`, per the
2026-08-05 A1 review fix), which builds the authority from
`WorldRevealCoordinator`/`RuntimeWorldTransitState` facts and calls
`TryExecuteAcceptedPortalArrival`; the identical Runtime entry point is
shared by the headless host. So: the type/`IsValid`/consumer-gate facts
pre-dated route 3 and were true before it; the arm (both the producer
that builds a live authority and the consumer that reads one) did not
exist before route 3 and is what the original bullet's "zero producing
call sites" language was pointing at.
- The exact-Setup mover chain (`PrepareMover` /
`RuntimeSetPositionMoverPreparer.TryBuild` /
`IPreparedCollisionSource.ReadSetupCollision`) exists piecewise, unwired.

View file

@ -206,25 +206,56 @@ which confirms that worker completion alone is not draw readiness.
### 2.1. Destination placement enters the spatial cell before simulation resumes
> **2026-08-04 correction (C4 route 3, D-T9), itself corrected 2026-08-05
> (R6 retail review):** the listing below attributes portal arrival to
> `player.enter_world(destination)`. That is wrong — a caller sweep of the
> named retail decomp
> (`docs/research/named-retail/acclient_2013_pseudo_c.txt:93770-93828`) shows
> both `CPhysicsObj::enter_world` call sites (pseudo-C `:93797` @0x004550EC
> and `:93824` @0x00455095) living inside **`SmartBox::HandleCreateObject`
> @0x00454C80** — `CObjectMaint::CreateObject` @0x00454FD8 is merely a
> *callee* it invokes partway through, not the enclosing function the first
> correction pass named. The two call sites are also **not both in the
> player branch**: @0x004550EC sits in the `if (arg3 != this->player_id)`
> NON-player branch (`PhysicsDesc::get_position``enter_world` for a
> newly-created REMOTE object); only @0x00455095 sits in the player branch,
> after `SmartBox::init_player` + `CellManager::ChangePosition`. Both sites
> are the LOGIN/CreateObject path that creates a physics object for the
> first time — neither is portal arrival. Portal arrival is
> `SmartBox::TeleportPlayer` (`0x00453910`) → `CPhysicsObj::SetPositionSimple`
> (`0x00453924`/`0x005162B0`) — confirmed by C4 route 3's own §1 citations
> and grep at `acclient_2013_pseudo_c.txt:92514-92521`. The conclusion below
> (commit the cell before releasing simulation) is unaffected —
> `SetPositionSimple` reaches the identical `change_cell`/`update_object`
> machinery this section describes — only the entry-point name and
> pseudocode's `enter_world` call are wrong; read `SetPositionSimple(destination)`
> wherever this section says `enter_world(destination)`.
>
> This routing is `SmartBox::TeleportPlayer``SetPositionSimple`
> everywhere; nothing in the passages below distinguishes retail's specific
> Recall/Lifestone/GM-teleport CAUSES, since they all funnel through the same
> accepted-destination Position at this layer.
Named retail references:
- `CPhysicsObj::change_cell` at `0x00513390`
- `CPhysicsObj::update_object` at `0x00515D10`
- `CPhysicsObj::enter_world` at `0x00516170`
- `SmartBox::TeleportPlayer` at `0x00453910`
- `CPhysicsObj::SetPositionSimple` at `0x005162B0`
- `CPhysicsObj::prepare_to_enter_world` at `0x00511FA0`
- `CPhysicsObj::set_hidden` at `0x00514C60`
Retail does not separate an accepted destination Position from the object's
live cell pointer. `enter_world` runs `SetPosition`, which installs the object
in its destination `CObjCell`, before the PartArray and MovementManager
enter-world boundaries complete. `update_object` then rejects only a parented
object, a null `cell`, or a Frozen object; Hidden is not a reason to skip the
live cell pointer. `SetPositionSimple` installs the object in its destination
`CObjCell`, before the PartArray and MovementManager enter-world boundaries
complete. `update_object` then rejects only a parented object, a null `cell`,
or a Frozen object; Hidden is not a reason to skip the
ScriptManager/ParticleManager tail.
```text
accepted portal destination becomes ready:
player.enter_world(destination)
SetPosition(destination)
SmartBox.TeleportPlayer(destination)
SetPositionSimple(destination)
change_cell(destination CObjCell)
PartArray.HandleEnterWorld()
MovementManager.HandleEnterWorld()

View file

@ -0,0 +1,411 @@
# C4 route 3 — architecture / adversarial review, ROUND 2 (delta) — 2026-08-05
**Verdict: FAIL.**
Reviewed: the uncommitted working tree at HEAD **`cd3129e9`**, +2,570/-232 across
16 files. Round-1 report:
[`2026-08-04-c4-route-3-architecture-review.md`](2026-08-04-c4-route-3-architecture-review.md).
`dotnet build AcDream.slnx -c Debug` exits 0.
**Round-1 findings closed: A4, A6, A9, A10(a).** A1, A2, A3 were addressed with
real design work that is directionally right — the A1 fix in particular
(inverting the readiness feed instead of touching the sequencer) is the correct
architectural answer to a hard constraint, and I want that stated plainly.
**The FAIL is one defect, present symmetrically on both hosts, introduced by
the A1/A3 fixes themselves:** both new "am I committed yet?" gates infer
*commit* from *the drive controller's global pending slot being empty*. That
slot empties on at least three paths that do **not** commit — including the one
the drive's own doc comment names as the *expected* outcome of a park. When it
does, the graphical controller latches `_placementCommitted = true` and the
headless projection reports `IsCollisionReady: true`, and both hosts then march
the full completion sequence against an unmoved body. That is round-1's A1/A3
restored, and on the graphical side it is now *worse*, because
`AcknowledgePortalMaterialized` succeeds where it previously failed its
invariant.
Numbering continues as **B*n*** to avoid collision with round 1.
---
## MAJOR — the FAIL
### B1 — `PendingCount == 0` is not "committed"; both hosts infer commit from a signal that is also set by three non-committing paths
**Severity: MAJOR (FAIL basis). Both hosts. Uncovered by any test.**
Graphical, `src/AcDream.App/Streaming/LocalPlayerTeleportController.cs:646-658`:
```csharp
if (_awaitingDeferredWake)
{
if (_acceptedPositionDrive.PendingCount != 0)
return false;
_awaitingDeferredWake = false;
_placementCommitted = true; // <-- infers commit from "not pending"
return true;
}
```
Headless, `src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs:816-821`:
```csharp
if (_awaitingPortalWake)
{
committed = _acceptedPositionDrive.PendingCount == 0;
if (committed)
_awaitingPortalWake = false;
}
```
`PendingCount` is
`RuntimeAcceptedPositionDriveController.cs:330``_pending is null ? 0 : 1`.
It is (a) **global**, not portal-scoped, and (b) cleared by every terminal path,
committing or not. `Advance()` clears `_pending` at five sites; three of them
run **without** a portal commit:
| site | condition | committed? |
|---|---|---|
| `:975` | *"The watch died — most likely a subsequent accepted Position's merge-time `Forget`"* | **no** |
| `:920` | A2's new abandon-at-wake (`!IsPortalAuthorityCurrent`) | body moved by `RetryDeferred`, **suffix skipped** |
| `:998` | A2's new abandon at the prepare-retry branch (`CancelToken`) | **no — nothing ever placed** |
**The `:975` path is the modal case, not a corner case.** The method's own
doc comment (`:880-892`) says it verbatim:
> `RuntimeEntityObjectLifetime.TryApplyPosition` calls `Forget` on EVERY
> accepted Position for this entity … ACE broadcasts at 5-10 Hz, so a
> `DeferredCell` park surviving past one broadcast interval is cancelled
> before its collision generation can ever commit it — **the exact
> far-destination case the park exists to serve**.
So: park → within ~100-200 ms an ordinary broadcast `Forget`s it → `Advance`
clears `_pending``PendingCount == 0` → both gates declare success.
**Concrete failure scenario (graphical).** Portal to a landblock whose
collision generation has not committed. `TryExecuteAcceptedPortalArrival`
returns `DeferredCell`; `_awaitingDeferredWake = true`. One ACE broadcast
later the park is Forgotten and `_pending` clears. Next `Tick`:
`_placementCommitted = true``placementReady = true` → the sequencer leaves
`Tunnel` and fires `Place` → the `if (!_placementCommitted) return;` guard at
`:557` **passes**`_placement.Place(_pendingRotation)` writes the render
entity from the *unmoved* `controller.Position` and rebuckets to the *source*
cell → `ObserveMaterialized(_pendingRevealGeneration, sequence, _pendingCell)`
**succeeds** (the reveal is still active and current) → `PlayExitSound` reveals
the world viewport → `FireLoginComplete` sends LoginComplete, and
`_transit.Complete(generation)` now **passes** its
`portal-complete-before-materialized` check because materialization was falsely
acknowledged. The player is released into the world standing at the
pre-teleport position, the transit reports a clean completion, and nothing logs
an invariant failure. Round 1's A1 at least tripped
`FailInvariant("portal-complete-before-materialized")`; this does not.
**Concrete failure scenario (headless).** Identical shape:
`PrepareDestination` returns `IsCollisionReady: true`, so
`RuntimeLiveEntitySessionController.TryAdvancePortalCompletion` runs
`AcknowledgeDestinationReadiness``AcknowledgePortalMaterialized`
`Complete``TerminalProjected``LoginComplete``EndTeleport`, and sets
`controller.State = PlayerState.InWorld`, all with the body unmoved. That is
round-1 A3 verbatim.
**Secondary hazard from the same root:** because the slot is global, a portal
park killed by a merge-time `Forget` can be immediately replaced by the force
arm's own `RetainPending` from that same merge
(`TryExecuteAcceptedLocalPosition``RetainPending`). The portal gate then
polls a **ForcePosition** operation, waits for it, and latches "portal
committed" when the force operation settles.
**Why no test caught it.** Both new park tests
(`PortalDeferredCell_ParksThenCommitsExactlyOnceOnTheCollisionGenerationWake`,
`HeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake`)
commit the destination's collision generation so the park resolves by
committing. `PortalDeferredCell_WakeAbandonsInsteadOfReconcilingWhenAuthorityWentStale`
asserts the **drive's** behaviour (`Assert.Empty(gameActions)`) and stops
there — it never asks what the *host gate* concludes from the resulting
`PendingCount == 0`. The seam between "the drive retired the park without
committing" and "the host decides the portal is placed" is exactly where the
defect lives and is exactly what no test crosses.
**Fix direction.** Stop inferring. The drive already knows the answer with
certainty — `ReconcileAndAcknowledgePortal` runs on commit and only on commit.
Publish that fact:
- add a portal-commit observable to `RuntimeAcceptedPositionDriveController`
— e.g. `bool TryConsumePortalCommit(long revealGeneration, ushort teleportSequence)`
latched in `ReconcileAndAcknowledgePortal` and cleared on consumption, or an
`Action<RuntimePortalPlacementAuthority>` commit callback supplied at
construction alongside `isPortalAuthorityCurrent`;
- have both host gates consume **that**, keyed on the reveal
generation/sequence they are waiting for, so a force pending, a Forgotten
park, and an abandoned park are all correctly "still not committed";
- give the park a terminal "abandoned" outcome the host can see, so it can
either re-attempt cleanly (`_awaitingDeferredWake = false` and try Begin
again next tick) or converge through the existing transit cancellation
rather than silently succeeding.
**Required tests (both must fail against the current code):**
1. Graphical: park, then kill the park with a merge-time `Forget` (an ordinary
accepted Position — the fixture's `OfferDestination` already performs the
merge) instead of committing the collision generation; drive 100 ticks;
assert `Placement.Called == false`, `Movement.Controller.Position` unchanged,
`Reveal.PortalMaterializationCount == 0`, `Session.LoginCompleteCount == 0`,
`Controller.IsActive == true`.
2. Headless: same, asserting `PrepareDestination` keeps returning
`IsCollisionReady: false` and `controller.State` stays `PortalSpace`.
3. Graphical: park, then let the force arm take the pending slot; assert the
portal gate does not latch when the *force* operation settles.
---
## MEDIUM
### B2 — A2's re-validation does not close the FIFO wedge it was written for; it only narrows reachability
The implementer's own note is accurate and I confirm it: `RetryDeferred`
`CommitCanonical` publishes the `Place` receipt asynchronously, and
`IsPortalAuthorityCurrent` runs only afterwards, inside `Advance`, gating the
*suffix*. But the wedge was never in the suffix — it is in the receipt:
- `Advance:920`'s abandon branch is reached only **after**
`TryPeekAcknowledgedPlacement` succeeded, i.e. the sink **already accepted**
the receipt. On that path there was never a wedge to prevent.
- The wedge path is the one where the sink **refuses**:
`RuntimePlacementPresentationSink.TryApply`
`RuntimeWorldTransitState.IsCurrentPlacementAuthority` false → return false →
`RuntimePlacementProjectionSubscription.OnPlacement` leaves it at the FIFO
head → every later placement receipt for every entity is blocked and the
drive's pending never converges. `IsPortalAuthorityCurrent` never runs on
that path, because `TryPeekAcknowledgedPlacement` never yields.
What the A1 fix *does* buy is reachability: with `placementReady` false during
a park, the sequencer cannot reach `FireLoginComplete`, so the transit no
longer ends underneath an outstanding park in the ordinary flow. The remaining
entries are mid-transit supersession (a second F751 while parked →
`OnTeleportStarted``ResetTransit``EndTeleport`), `ResetSession`, and
`ResetGenerationPresentation` — i.e. exactly the §9 "honest gap" cases.
That is a genuine narrowing and I credit it. It is not closure, and the
consequence remains unbounded (whole-FIFO stall + non-convergent shutdown), so
contract stop condition 4 ("P3 finds a portal receipt no mechanism can consume
or retire — the FIFO-wedge shape changes the design, not the test") still
applies.
**Fix direction.** The retire path has to exist at the receipt, not the suffix.
Either (a) let the sinks treat a `Place` whose entity/versions are current but
whose portal authority is dead as acknowledge-and-ignore (the same shape
`Discard`/`ExecutorCompleted`/`WithdrawalRestored` already use, and for the
same stated reason — refusing wedges the ordered stream), or (b) have
`CommitCanonical` drop a portal suffix it can already see is not current rather
than publishing a receipt nothing can consume. (a) is smaller and matches the
existing precedent in both sinks' own doc comments.
### B3 — Headless `_awaitingPortalWake` is not reset across teleports within one session
`HeadlessSessionWorldProjection.cs:762`. The graphical twin
(`_awaitingDeferredWake`) is cleared in `ResetTransit:932`, which
`OnTeleportStarted` calls — clean. The headless field has no equivalent: it is
cleared only when the poll declares success, and `HeadlessSessionWorldProjection`
is constructed per *session*, not per teleport.
**Scenario.** Teleport 1 parks → `_awaitingPortalWake = true`. Before the next
`PumpPortalCompletion`, a new F751 arrives; `TryCompletePortal` overwrites
`_pendingPortalCompletion` with reveal 2. The next pump calls
`PrepareDestination(reveal 2)`, which takes the **poll** branch left over from
reveal 1 — so reveal 2's placement is never even attempted, and if
`PendingCount` happens to be 0 it is immediately declared committed. This is
B1's inference bug plus a stale latch, on a path that does not require a
Forgotten park.
**Fix direction.** Key the latch to the reveal generation (or clear it in
`BeginTeleport`, which already runs per teleport on this host).
### B4 — Both hosts' retry loops are unbounded and undiagnosable
A refusal loop (`Contention`, stale reveal, or a park that never converges)
now retries forever with no timeout and no terminal path.
- **Graphical**: the player holds in the tunnel with the retail wait cue after
5 s. This is a *modelled* end state — AD-2 already documents "predicate never
satisfies → portal transit remains in the authored tunnel and presents the
centered wait cue" — so an infinite stall is strictly better than round 1's
silent release, and I do not consider it blocking. Two gaps, though: AD-2
attributes that state to streaming/DAT failure only, and now a *placement*
refusal produces the identical user-visible state; and `_holdSeconds` /
`ObserveWait` are now driven by `!placementReady` rather than `!dataReady`,
which is a real semantic change to the wait cue's meaning. The
`[tp-probe] REFUSED cause=…` line does distinguish them in the log — good —
but AD-2 should say so.
- **Headless**: worse, because there is no cue and no bound. A bot whose
destination collision never becomes resident sits in `PlayerState.PortalSpace`
indefinitely, connected and healthy-looking; `PumpPortalCompletion` is
entirely silent. Under K4's 30-session envelope this is an invisible stuck
session. At minimum emit one probe line on the first N retries; ideally bound
the wait and fail loudly.
### B5 — §8 items 8/9/10 (the committed-receipt presentation suite) — **does NOT block**, with conditions
Answering the coordinator's direct question.
**Does not block route 3.** Reasons, in order of weight:
1. The render-entity half now has a *proven mechanism*, not a claim.
`LiveEntityRuntime.TryApplyRuntimePlacementPlace:1394-1402` performs
`entity.SetPosition(projection.WorldPosition)`, `entity.Rotation = …`,
`entity.ParentCellId = token.ExactCellId` and `RebucketLiveEntity`,
synchronously inside `CommitCanonical`, before `TryPublishPlace` snapshots.
That path is shared with route 2's force arm and C3c's first entry and has
existing coverage. Round 2 correctly rewrote the class doc to say so
(round-1 A10(a) closed).
2. The restored `Movement.Controller.Position`/`CellId` assertions prove the
canonical body resolved the offered destination — which is the half route 3
actually changed ownership of.
3. The shadow half is a **pre-existing route-2 defect**, not a route-3
regression: `LocalPlayerShadowState.Set` (written by the sink) updates only
the dedup cache, never `PhysicsEngine.ShadowObjects`, while
`LocalPlayerShadowSynchronizer.SyncPose` dedups against that same cache. It
self-heals on the player's first >1 cm move. Route 3 widens the window (via
`LocalPlayerProjectionController.Project:102`'s PortalSpace early return) but
does not change its kind.
**Conditions — all three, or it does block:**
- It is recorded as an **open issue with a number** (#312's layer / route 2's
B2 gap, now two campaigns old) and carried explicitly into C5's parity-test
scope. Not a comment; a tracked item.
- The connected gate is **not** scored as covering it. The probe fields
(`leash`, `autorun`, `hookTail`) say nothing about the shadow; there is no
visual for it. If the user's session passes, the shadow claim remains
test-verified-nowhere and must be reported that way (the §9 "honest gap"
discipline).
- The register gets one line under AD-2 or a sibling row naming the
cache-without-publish asymmetry, so the next reader does not assume
`LocalPlayerShadowState.Current` means "published".
**Concretely, what the test needs** (in
`tests/AcDream.App.Tests/World/RuntimePlacementPresentationSinkTests.cs`, whose
fixture already owns `BeginPortal` and a `LocalShadow`):
1. **Committed local-player portal Place through the real sink.** Pre-seed the
render `WorldEntity` at a *different* (wire) pose — this is also §8 item 9's
T8-ordering half. Drive a `Place` receipt for the local player carrying a
VALID portal authority. Assert, after: `entity.Position`/`Rotation`/
`ParentCellId` equal the receipt's `WorldPosition`/`Orientation`/
`ExactCellId` (the wire pose did not survive); the spatial bucket moved to
the destination landblock; `LocalShadow.Current` equals the resolved pose;
**and `PhysicsEngine.ShadowObjects` actually holds a row for the player at
the destination cell** — that last assertion is the one that discriminates
cache-only from published, and is the whole point.
2. **Discrimination half.** The same receipt with a stale/superseded portal
authority must be refused **and then retired** — not left at the FIFO head.
This is also B2's regression test.
3. **Refused-Place presentation (§8 item 10).** Already partly covered by
`RefusedPlace_HoldsTheStreamAndConvergesOnlyAfterContentionClears`; extend it
to assert the pre-teleport pose is still the *presented* pose (entity +
world snapshot store), not only that the body is unmoved.
4. **Sabotages that must fail:** remove the sink's `entity.SetPosition` → (1)
fails; leave the shadow write as cache-only → (1)'s `ShadowObjects`
assertion fails; make the stale-authority receipt return `false` forever →
(2) fails.
---
## MINOR
### B6 — `isPortalAuthorityCurrent` should be a required constructor parameter
The coordinator's specific concern, checked: **both production sites wire it**
`SessionPlayerComposition.cs:597-603` and `HeadlessSessionHost.cs:665-671`,
both to `RuntimeWorldTransitState.CanPlacePortalDestination`. Four test sites
do not, which is fine.
The residual is that a null default silently restores the round-1 defect, and
nothing catches a future production site that forgets it — there is no
architecture guard for this the way
`RuntimePhysicsOwnershipTests.ProductionHostsUseSharedPlacementSubscriptionWithoutDirectChannel`
guards the placement channel. With only 2 production + 4 test constructions,
making the parameter required (tests pass `static _ => true`) converts a silent
regression into a compile error for ~6 lines of churn. Same argument will apply
to B1's commit observable.
### B7 — `_placementCommitted` is checked once and never re-validated
`LocalPlayerTeleportController.cs:557`. Round 1's Place handler ran
`CanPlacePortalDestination` immediately before mutating; round 2 moved that
check into `TryAdvancePortalCommit`'s **non-deferred** branch only
(`:668-676`). Once `_placementCommitted` latches, the only guard before
`_placement.Place()` / `ObserveMaterialized` is `IsCurrentLifetime`. If the
reveal is cancelled between the commit and the Place event,
`ObserveMaterialized` refuses (`IsCurrentPortalDestination`) but the
presentation suffix has already run and the stream continues to
`FireLoginComplete` with an unmaterialized reveal. Much less severe than B1 —
the body genuinely is at the destination — but it is the same family. Cheap
fix: keep the `CanPlacePortalDestination` re-check in the Place handler
alongside `_placementCommitted`.
### B8 — A8 (round 1) confirmed still open and confirmed non-blocking
Five portal tests still call `ConvergePortalHost` as the last statement of the
test body rather than in a `finally`, so an assertion failure is still masked
by a `Dispose()` throw during unwinding. Correctly flagged rather than silently
dropped. Test hygiene only — no production effect. It does mean that when B1's
new tests are written, a genuine failure may again present as a teardown throw;
fixing the `try/finally` first would save that debugging round.
### B9 — round-1 A11/A12 unchanged
`PhysicsDiagnostics.LocalTeleportHostKind` process-global (accepted);
`AddSyntheticIndoorCell` geometry-free (accepted — and less load-bearing now
that A6's position assertions are restored).
---
## Closed since round 1
| round-1 finding | status | evidence |
|---|---|---|
| **A4** — route facts unread, inversions hardcoded | **CLOSED** | `RuntimeAuthoritativePositionRoute.RunsTeleportHook:164` / `.ConstrainAfterRouting:170`; `ReconcileAndAcknowledgePortal` reads both; `CommitCanonicalTeleportFrame(bool zeroVelocity, bool rearmConstraintLeash)` branches on them. `ConstrainPhase.None` sabotage now fails `PortalCommitted_MovesBodyArmsLeashOnceCancelsAutorunAndSendsExactlyOneMovementEvent`'s stale-anchor assertion, as the contract's §8 item 11 intended. |
| **A6** — superseded-teleport discriminator removed | **CLOSED** | `NewerStart_ReplacesOldDestinationWithoutReusingIt` asserts `Movement.Controller.Position.X/Y == 2` and the cell; `SameLandblockDestination_…` asserts `(20,30,4)` + `0x20210123`; the Z-vs-X/Y reasoning in the comment is sound. |
| **A9** — two sequence sources | **CLOSED** | The authority now uses `destination.TeleportSequence`, with a `Debug.Assert` pinning it equal to the caller's copy. |
| **A10(a)** — class doc claimed the suffix was the render entity's only mover | **CLOSED** | Doc rewritten to state the writes are redundant repeats of the canonical receipt's mutation, with the ordering cited. |
| **A1** — refused Place did not stop the anim stream | **partially** — the mechanism is right (readiness inversion, sequencer untouched), and the `Contention` path is now correctly held and tested (`RefusedPlace_HoldsTheStreamAndConvergesOnlyAfterContentionClears`). **B1 reopens it for the `DeferredCell` path.** |
| **A2** — no wake re-validation | **partially** — D-T2.4's re-validation now exists and is wired in both hosts and tested; **B2** shows it does not close the FIFO wedge. |
| **A3** — headless discarded the status | **partially** — status now honoured, throws on `Rejected`/`NotApplicable` and on a missing drive, retryable pump added. **B1/B3 reopen it.** |
| **A5** — presentation suite | **still open** — see B5 for the definite blocks/does-not-block answer. |
| **A7** — dual-host parity | **CLOSED** | `HeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake` drives a real wired drive end to end and asserts body position, cell, and `PlayerState`. |
| **A8** | open, non-blocking (B8). |
| **A11 / A12** | unchanged (B9). |
## Also verified this round
- `_pendingDestination` lifetime — round-1 judgment stands, not re-litigated.
- The sequencer's own invariants survive the late `worldReady`: `Tunnel` is
explicitly a hold state (*"Hold here until worldReady"*), `TickTunnel` still
runs on the hold path so `CurrentAnimationFrame` keeps advancing into
`TunnelContinue`'s exit window, and `maxForce` at 5 s covers a stale frame.
`worldReady` has exactly one consumer (`case TeleportAnimState.Tunnel`), so
no other transition changed meaning.
- No double-Begin headless: `TryCompletePortal`'s own
`TryGetAcceptedTeleportDestination`/`TryBeginPortalReveal` prefix cannot
succeed twice for one reveal (the destination slot is consumed), and
`_awaitingPortalWake` suppresses a second concurrent `Begin` while parked.
(The stale-latch problem is B3, a different failure.)
- `_placementCommitted` / `_awaitingDeferredWake` are both cleared in
`ResetTransit:931-932`, so the graphical latches do not survive a new F751,
a session reset, or a generation reset.
- Build green.
---
## Summary
| # | Severity | Finding |
|---|---|---|
| B1 | MAJOR | `PendingCount == 0` inferred as "committed" on both hosts; three non-committing paths clear it, including the drive's own documented modal outcome. Reintroduces A1 (graphical, now with a *successful* false materialization) and A3 (headless). Untested. |
| B2 | MEDIUM | A2's re-validation gates the suffix, not the receipt; the FIFO wedge is narrowed (supersession/reset only) but not closed. |
| B3 | MEDIUM | Headless `_awaitingPortalWake` is not reset per teleport; a stale latch can skip reveal N+1's placement attempt entirely. |
| B4 | MEDIUM | Unbounded, undiagnosable retry loops on both hosts; headless has no cue, no bound, and no log. |
| B5 | — | §8 8/9/10 presentation suite: **does not block**, subject to three stated conditions; required test spelled out. |
| B6 | MINOR | Make `isPortalAuthorityCurrent` (and B1's commit observable) required constructor parameters. |
| B7 | MINOR | `_placementCommitted` never re-validated before the presentation suffix. |
| B8 | MINOR | A8 still open (5 tests), confirmed non-blocking. |
| B9 | MINOR | A11/A12 unchanged. |

View file

@ -0,0 +1,550 @@
# C4 route 3 — architecture / adversarial review (2026-08-04)
**Verdict: FAIL.**
Reviewed: the uncommitted working-tree diff (`git diff HEAD` + untracked) on
`claude/acdream-physics-divergence-5aa784` at HEAD **`cd3129e9`** (route 7's
commit, "child cell propagation moves from a render tick into Runtime"). 16
files, +1,573/-216.
Reference documents read in full: the route-3 contract
(`2026-08-04-c4-route-3-contract.md`), the route-3 scoping, the route-2
contract, and the route-5/route-7 review defect classes.
Independent verification performed against source, not against the
implementer's summary: `RuntimeWorldTransitState`, `RuntimeSetPositionState`'s
commit tail, `RuntimeEntityObjectEventStream`/
`RuntimePlacementProjectionSubscription` (publication synchronicity),
`RuntimePlacementPresentationSink` + `LiveEntityRuntime
.TryApplyRuntimePlacementPlace`, `LocalPlayerShadowSynchronizer`,
`LocalPlayerProjectionController`, `TeleportAnimSequencer`, and both hosts'
`Advance()` pump sites. `dotnet build AcDream.slnx -c Debug` exits 0.
**The `_pendingDestination` fix — the item flagged as highest risk — is
correct, and is not the reason for the FAIL.** See §"Judgment on the
`_pendingDestination` lifetime" at the end.
The FAIL rests on A1 and A2: the slice adds five new ways for the Place edge
to refuse, and the refusal path it hands them to does not stop the teleport
animation stream. One of those five (`DeferredCell`) additionally commits the
placement out of band after the transit has ended, which both splits the body
from presentation and leaves a placement receipt nothing can consume — proof
obligations **P3** (graphical half) and **P4** are undischarged, and D-T2.4's
"the wake path must re-validate the portal authority before committing" is not
implemented at all.
---
## MAJOR
### A1 — A refused canonical Place does not stop the teleport animation; the player is released into the world without ever having been placed
**Severity: MAJOR (FAIL basis).**
`src/AcDream.Core/World/TeleportAnimSequencer.cs:134-142`:
```csharp
case TeleportAnimState.Tunnel:
if (worldReady)
{
evts.Add(TeleportAnimEvent.Place);
Advance(TeleportAnimState.TunnelContinue, enterTunnel: false);
_continueElapsed = 0f;
}
break;
```
`TeleportAnimEvent.Place` is emitted **exactly once**, and the sequencer
advances to `TunnelContinue` in the same statement block, unconditionally and
with no knowledge of whether the consumer's handler succeeded. There is no
path back to `Tunnel`.
`src/AcDream.App/Streaming/LocalPlayerTeleportController.cs:505-526`:
```csharp
case TeleportAnimEvent.Place:
if (!_worldReveal.CanPlacePortalDestination(...)) return;
if (!TryExecuteCanonicalPortalPlacement(sequence))
return; // <- new in this slice
...
_placement.Place(_pendingRotation);
...
_worldReveal.ObserveMaterialized(...);
```
The `return` exits `Tick`, but the sequencer has already left `Tunnel`. Every
subsequent `Tick` therefore runs the rest of the stream with **no placement
and no materialization**:
| next event | what runs |
|---|---|
| `TunnelContinue``TunnelFadeOut` | — |
| `PlayExitSound` (`:532-542`) | `_worldReveal.RevealWorldViewport()` + `_presentation.ExitTunnel()` |
| `FireLoginComplete` (`:543-554`) | `_mode.EnterWorld()`, `_session.SendLoginComplete()`, `_worldReveal.Complete()`, `ResetTransit(clearSession: false)` |
`WorldRevealCoordinator.RevealWorldViewport` only needs a live host projection
(`WorldRevealCoordinator.cs:258-266`) — present. `Complete()` reaches
`RuntimeWorldTransitState.Complete` (`:701-706`), which hits
`FailInvariant("portal-complete-before-materialized")` and returns `false`;
`FailInvariant` (`:934-946`) only increments a counter and logs — it does not
throw and does not stop the caller. `ResetTransit` then runs `EndTeleport()` +
`_worldReveal.Cancel()`.
**Concrete failure scenario.** A `Contention` outcome (route 2's force arm, a
route-7 parent drive, or an earlier park still owns the entity's placement
token at the Place edge) makes `TryExecuteAcceptedPortalArrival` return
`Contention` at `RuntimeAcceptedPositionDriveController.cs:492-501`. The
player watches the portal tunnel finish normally, the world viewport is
revealed, `LoginComplete` is sent to ACE — and the player is standing at the
**pre-teleport position** in the **pre-teleport cell**. ACE has them at the
destination. Every subsequent server broadcast fights the client. Nothing in
the client logs above `[world-reveal] event=invariant-failure` (a `SafeLog`
line), and the D-T8 probe never emits because
`ReconcileAndAcknowledgePortal` never ran.
**Why this is the slice's problem and not inherited.** The pre-existing
`CanPlacePortalDestination` early return (`:506-512`) has the same shape, but
it fires only when the transit no longer owns this reveal — a case where
marching on is at worst redundant, because a newer transit owns the world.
This slice adds **five new refusal causes that all fire while the transit is
perfectly healthy**: `host-token-unavailable`, `NotApplicable`, `Rejected`,
`Contention`, and `DeferredCell` (`LocalPlayerTeleportController.cs:596-625`,
which treats everything except `Committed` as a refusal at `:619`).
**Contract obligations violated.** §4 item 4 ("on every refusal … the transit
remains coherent … never a half-state … no path leaves the player permanently
in portal space with a dead operation"); §4 item 5 ("a refused placement must
NOT … `RevealWorldViewport`, must NOT advance the anim-event stream's terminal
events"); D-T5's Begin-refusal row ("the anim stream stays where it is, so the
NEXT Tick re-attempts the Place edge … if the Place anim event is one-shot,
the re-attempt must be driven by the same Tick predicate that produced it, and
THAT mechanism must be stated in the commit"); and proof obligation **P4**
verbatim. P4 was to be discharged by reading `TeleportAnimSequencer`. It is
one-shot. No re-attempt driver exists.
**Fix direction.** Two shapes are available without touching the sequencer
(stop condition 2 forbids sequencer timing changes):
1. Make the Place edge idempotent-and-latched at the controller: keep a
`_placementCommitted` flag; on a refusal, do NOT let the stream reach
`PlayExitSound`/`FireLoginComplete` — gate those two cases on the latch and
drive a bounded re-attempt from the same `ready` predicate that produced
the Place event (the contract's stated fallback). A refusal that never
converges must then take the existing transit cancellation
(`ResetTransit(clearSession: false)` — which already cancels the reveal and
restores presentation) rather than a silent world release.
2. Or treat a refusal as an immediate transit cancellation and let the
existing supersession path own recovery. Louder, smaller, and it satisfies
D-T5's "never a silent wedge" — but it needs the user's eyes because it is
user-visible (the portal fails and the player stays put) rather than a
silent desync.
Either way this needs a test: *refused arm → the anim stream does not reach
`RevealWorldViewport`/`FireLoginComplete` with an unplaced body.*
---
### A2 — A `DeferredCell` portal park splits the body from presentation and leaves a placement receipt nothing can consume (P3 undischarged on the graphical host; D-T2.4's re-validation missing)
**Severity: MAJOR.**
`RuntimeAcceptedPositionDriveController.SubmitAndResolvePortal:614-645` parks a
`DeferredCell` outcome into `_pending` carrying the portal authority.
`LocalPlayerTeleportController.TryExecuteCanonicalPortalPlacement:619` returns
`false` for it, so A1's march runs: the stream reaches `FireLoginComplete`,
`ResetTransit` calls `_transit.EndTeleport()` and `_worldReveal.Cancel()`.
The park is still live. `Advance()` is pumped by the graphical host at
`src/AcDream.App/Net/GraphicalSessionEventRoute.cs:117` and
`src/AcDream.App/World/LiveEntityHydrationController.cs:416`. When the
destination landblock's collision generation eventually commits:
- `Advance:763-771` runs `ReconcileAndAcknowledgePortal` — the body moves, the
leash re-arms, autorun cancels, and **one outbound movement event is sent**
seconds after the player was already released into the world at the old
position. Presentation is never told: `_placement.Place` and
`ObserveMaterialized` are unreachable (the anim event is one-shot, A1).
- The commit publishes a `Place` receipt whose `Token.Portal` still names the
ended reveal. `RuntimeWorldTransitState.IsCurrentPlacementAuthority:258-275`
requires `IsCurrentPortalDestination` (`:870-882`), which requires
`_teleportActive` — cleared by `EndTeleport` (`:547-556`). It returns false
**forever**.
- `RuntimePlacementPresentationSink.TryApply:100-106` therefore returns
`false`; `RuntimePlacementProjectionSubscription.OnPlacement:134-136` leaves
the receipt at the FIFO head. Every later placement receipt **for every
entity** is blocked behind it, and `AcceptedPositionDrivePendingCount` never
returns to zero, so `GameWindowLifetime.DisposeGameRuntime` throws on
shutdown.
That is precisely the failure mode P3 exists to rule out ("A receipt nothing
can ever consume or retire is a FIFO wedge"). P3 was discharged only for the
headless *happy path*; the graphical park was not walked.
Independently, **D-T2.4's explicit requirement is not implemented**: "its wake
path must re-validate the portal authority before committing". Neither
`Advance`'s `AwaitingCommitWake` branch (`:763-771`) nor its
`IsPlacementCurrent` re-submit branch (`:813-819`) re-checks the portal
authority — they pass `pending.Portal` straight through.
**Fix direction.** (a) Re-validate the portal authority at both wake points
(`_entityObjects` has no transit handle today — the drive needs a
`Func<bool>`-style currency predicate or the authority passed back through the
transit owner); on failure take `AbandonPending`'s exact shape
(`restoreCancelledPark: true` + `PublishCancellation`) so the park is retired
rather than committed. (b) Independently, A1's fix must prevent the transit
from ending while a portal park is outstanding.
---
## MEDIUM
### A3 — Headless discards the arm's status entirely; a failed placement is silent
`src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs:768-780`:
```csharp
if (_acceptedPositionDrive is not null)
{
var authority = new RuntimePortalPlacementAuthority(...);
_ = _acceptedPositionDrive.TryExecuteAcceptedPortalArrival(destination, authority);
}
```
Two problems. First, the status is discarded: any non-`Committed` outcome
leaves the body unmoved while `TryCompletePortal`
(`RuntimeLiveEntitySessionController.cs:530-585`) proceeds through
`AcknowledgeDestinationReadiness``AcknowledgePortalMaterialized`
`Complete``TerminalProjected``LoginComplete``EndTeleport` — asserting
a materialization that did not happen (§4 item 5) and telling ACE the login
completed. The deleted `ResynchronizeLocalPlayerForPortalArrival` was
unconditional and could not fail this way. Second, the `is not null` guard
means a composition regression that fails to wire the drive silently disables
headless portal placement with no signal at all; the previous code had no such
mode.
**Fix direction.** Treat a non-`Committed` status as a hard failure on this
path (`TryCompletePortal` already throws on every other Runtime refusal —
match that), and make the drive a required constructor dependency for the
production projection.
### A4 — Both inversions are hardcoded; the classifier route facts that encode them are never read
`ReconcileAndAcknowledgePortal(RuntimeEntityRecord, in RuntimeAuthoritativePositionRoute route, in RuntimePortalPlacementAuthority)`
(`RuntimeAcceptedPositionDriveController.cs:667-698`) **never references
`route`**. `PlayerMovementController.CommitCanonicalTeleportFrame:1987-2035`
unconditionally zeroes velocity, runs `StopCompletelyAtPhysicsObjectBoundary`,
`UnStick`/`UnConstrain`, and `RearmConstraintLeashAtCurrentPosition`.
So `route.ZeroVelocity`, `route.ConstrainPhase`, and `route.TeleportHookPhase`
are recorded-not-consumed, even though D-T2.3 pinned "`route.ZeroVelocity` is
honored at the commit". The visible behaviour is correct today only because
the classifier's LocalPlayer-teleport branch happens to agree with the
hardcoded method.
Consequence for test quality: the contract's own §8 item 11 sabotage —
"hardcode the force route onto the portal arm (`ConstrainPhase.None`) → test
5a fails" — **cannot fail**, because no code path reads `ConstrainPhase`. No
test discriminates the classifier from the executor. A future classifier edit
(the classifier is a shared surface routes 2/4b-2/4b-3 also consume) diverges
from behaviour silently.
**Fix direction.** Either consume the route facts in the frame commit (branch
on `ZeroVelocity`/`ConstrainPhase`) and add the discriminating test, or delete
the unused `route` parameter and state explicitly, in the class doc and in
AD-2, that the inversions are enforced by `CommitCanonicalTeleportFrame` and
NOT by the classifier — so the next reader does not trust a route fact that
nothing reads.
### A5 — Contract §8 item 8's committed-receipt presentation suite is missing; invariant 6's "the local-player collision shadow agrees" is asserted nowhere
No test in this diff drives a committed portal placement through the **real**
`RuntimePlacementPresentationSink` plus the suffix.
`tests/AcDream.App.Tests/World/RuntimePlacementPresentationSinkTests.cs` is
untouched; its only portal test
(`PortalPlace_RequiresExactCurrentTransitHostAndSequence:467`) drives a
synthetic token, not a local-player placement. The App teleport tests assert
`harness.Placement.Called` against a *fake* placement.
That missing suite would have surfaced the following latent inconsistency,
which route 3 newly makes reachable on the portal path:
- `RuntimePlacementPresentationSink.TryPublishPlace:226-232` writes
`_localPlayerShadow.Set(entity.Position, entity.Rotation, record.FullCellId)`.
`LocalPlayerShadowState.Set` updates **only the cache** — it never touches
`PhysicsEngine.ShadowObjects`.
- `LocalPlayerShadowSynchronizer.SyncPose:59-70` dedups against that same
cache (`cellId` equal AND position within 1 cm AND orientation within
tolerance ⇒ return without publishing).
- `LocalPlayerProjectionController.Project:102` early-returns for
`PlayerState.PortalSpace`, so the sink's cache write is the last word until
the player re-enters the world.
Net: the first post-arrival `SyncShadow` sees a cache that already claims the
resolved pose and resolved cell, skips, and the player's collision shadow row
is not published at the destination. It self-heals the first time the player
moves more than ~1 cm, so the window is short — but during it, other entities
have no collider for the player at the destination. (The shape is pre-existing
from route 2's force arm, where `Project` runs every frame so the window is
one frame; route 3's portal-space skip widens it.)
**Fix direction.** Write the §8 item 8 suite. Independently, either have the
suffix re-publish the shadow through `LocalPlayerShadowSynchronizer`
(`force: true`) after writing the resolved entity pose, or stop the sink from
writing a "last published" cache entry it did not publish.
### A6 — The one test that discriminated *which* destination gets placed lost its discriminator
`tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs`,
superseded-teleport scenario:
```diff
- Assert.Equal(new Vector3(2f), harness.Placement.Position);
+ Assert.True(harness.Placement.Called);
```
The old assertion proved the **second** destination `(2,2,2)` was placed and
not the first `(1,1,1)`. `Assert.True(Called)` cannot distinguish them. That is
exactly the property the `_pendingDestination` caching change puts at risk
(caching an Aim-time value instead of re-reading). Two sibling tests lose the
same class of assertion, and one now contradicts its own name:
`SameLandblockDestination_DoesNotRecenterAndKeepsTranslatedPosition` no longer
asserts any position or cell.
This is the route-5 "tests that assert only negatives"/weakened-successor
defect class. The replacement assertion is **available**: the harness now owns
a real Runtime `RuntimeLocalPlayerMovementState`, so
`Assert.Equal(expected, movement.Controller!.Position)` and `.CellId` are
reachable; the harness simply does not expose `movement`.
**Fix direction.** Expose the Runtime controller on the harness and restore a
positive position/cell assertion in each of the three tests, at minimum in the
superseded-teleport one.
### A7 — Dual-host parity (§8 item 6) is not met; the headless flip is untested
`tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs:413-424` now
documents that the fixture's projection is built without a drive controller,
so "the canonical portal arm this method now calls is a no-op here by
construction", and defers the headless committed-portal test as "an open item,
not attempted here given this session's time budget".
The contract states plainly: "The headless arm reuses the identical Runtime
entry — dual-host parity is a test obligation, not an aspiration (§8)."
Combined with A3, the entire headless production placement flip — the deletion
of `ResynchronizeLocalPlayerForPortalArrival`, the new `portal` parameter, the
new call — has zero behavioural coverage. The only surviving assertion change
is `CenterCount` 3→2.
---
## MINOR
### A8 — Portal-test cleanup runs at the end of the test body, so the first assertion failure is masked by a teardown throw
`ConvergePortalHost` is invoked as the last statement of each portal test
(`RuntimeAcceptedPositionDriveControllerTests.cs:1273/1380/1423/1519`). If any
earlier `Assert` throws, cleanup is skipped, `StartedRuntime.Dispose()` throws
during unwinding, and C# `using`/`try…finally` lets the finally-exception
**replace** the in-flight one.
The implementer diagnosed their own instance of this correctly (claim 1 —
verified: the mechanism is real and the two corrected assertions were genuine
test bugs, since an accepted `TeleportAdvanced` merge rebases the world frame
onto the destination per #283, and `ReconcileAndAcknowledgePortal` does
legitimately send one movement event). But the pattern remains in the shipped
tests, and it is exactly how a genuine host-projection leak would also present
— which is why it is worth removing rather than remembering.
**Fix direction.** `try { … } finally { ConvergePortalHost(…); }`, or make
`StartedRuntime.Dispose` record non-convergence and assert it explicitly.
`ConvergePortalHost` itself is otherwise sound: it cannot double-release —
`AcknowledgeHostProjection`'s `TerminalProjected` branch removes the record
(`RuntimeWorldTransitState.cs:343-355`) and a second call returns false — and
it cannot leak, because a forgotten call throws at Dispose.
### A9 — Two sources for one teleport sequence
`LocalPlayerTeleportController.cs:613` builds the authority's
`TeleportSequence` from `_transit.ActiveTeleportSequence` (passed in as
`sequence`), while `ClassifyPortalArrival`
(`RuntimeAcceptedPositionDriveController.cs:514-535`) derives its
accepted/prior pair from `destination.TeleportSequence`. They agree today —
`OfferTeleportDestination:490-497` refuses a second destination for an
already-accepted active sequence — but two sources for one fact is the
campaign's "mapping written against one caller's reachable set" shape.
**Fix direction.** Use `destination.TeleportSequence` in both, or assert
equality at the producer.
### A10 — Two documentation statements assert behaviour the code does not have
(a) The new class doc on `LocalPlayerTeleportPlacement`
(`LocalPlayerTeleportController.cs:188-193`) says the sink "snapshots whatever
the entity already holds and writes no pose itself — this is the render
entity's mover". `TryPublishPlace` writes no pose, but the sink's own upstream
call chain does: `RuntimePlacementPresentationSink.TryApply:108`
`LiveEntityRuntime.TryApplyRuntimePlacementProjection:1301`
`TryApplyRuntimePlacementPlace:1386-1420`, which performs
`entity.SetPosition(projection.WorldPosition)`, `entity.Rotation = …`,
`entity.ParentCellId = token.ExactCellId`, **and** `RebucketLiveEntity` — all
before `TryPublishPlace` snapshots. The suffix's entity writes and rebucket are
therefore redundant repeats of a mutation the canonical receipt already made.
This is harmless at runtime today, but it is the "a doc asserting behaviour
the code does not have" class — and the contract carries the same misreading
(§3.4, D-T4, §12.5(b)), so correcting the code comment alone is not enough.
(b) The plan correction in `docs/plans/2026-08-02-placement-cutover.md` says
"`RuntimeAcceptedPositionDriveController`'s portal arm reading it — was
already live before route 3 (from route 2's shared drive controller)". The
portal arm was added by **this** slice. A correction that itself asserts a
false fact is worse than the line it corrects.
### A11 — `PhysicsDiagnostics.LocalTeleportHostKind` is a process-global mutable set from a host
`src/AcDream.Core/Physics/PhysicsDiagnostics.cs` + `HeadlessSessionHost.cs:625`.
Correct under K3/K4 (all sessions in a headless process are headless), and the
doc comment says so — noted only so it is not later mistaken for per-session
state. No action required this slice.
### A12 — `AddSyntheticIndoorCell` is representative enough to pass the gate, and no more
The helper registers a `CellPhysics` with an empty `Resolved` polygon
dictionary, one `PortalInfo(0,0,0)`, and a leaf-only BSP root. It is not shaped
to make a specific assertion pass — it mirrors
`RuntimeSetPositionStateTests.AddSyntheticCell` and its only effect is to make
`PhysicsEngine.IsSpawnCellReady` return true for an indoor cell, which is a
genuine fixture gap (a bare `AddLandblock` passes an empty `CellSurface` list,
so indoor destinations parked `DeferredCell` forever). Accepted.
The caveat: because the destination cell has no geometry, the App-layer tests
prove "the arm returned `Committed`", not "the destination resolved somewhere
sane". That makes A6's missing position assertions more load-bearing, not
less.
---
## Verified correct (checked against source, not taken on report)
- **P1 (the D-T3 duty map).** `RuntimeSetPositionState.cs:5036-5058` calls
`PhysicsObjUpdate.CommitSetPositionContactTransition` unconditionally inside
the canonical commit, and `CommitSetPositionContactPrefix`
(`PhysicsObjUpdate.cs:153-175`) derives `Contact`/`OnWalkable`/
`WaterContact` from the placement result's own `InContact`/`OnWalkable`. Not
re-seeding `TransientState` in `CommitCanonicalTeleportFrame` is correct and
is more faithful than `SetPositionCore`'s unconditional
`Contact|OnWalkable|Active` overwrite, exactly as claimed.
- **P3, happy path, both hosts.** `RuntimeEntityObjectEventStream
.PublishPlacement:164-171` → `EnqueueAndDrain` →
`RuntimePlacementProjectionSubscription.OnPlacement:122-150` is
**synchronous**, inside `CommitCanonical`. Headless's placement therefore
commits and its receipt is consumed inside `PrepareDestination`, strictly
before `AcknowledgePortalMaterialized`/`Complete`/`EndTeleport`. The
receipt-past-`EndTeleport` hazard is discharged for the committed path. (The
residual is A2's park.)
- **Trap T7 / route-2 blast radius.** No portal pending reaches
`SettlePending` or `_newestForce`: the three `pending.Portal.Present` guards
at `:763`, `:794`, and `:820` fence every terminal path, and
`SubmitAndResolvePortal` is a genuine sibling of `SubmitAndResolve` rather
than an overload of it. `git diff` shows **zero** expectation changes in any
force-arm test — the contract's §4 item 8 tripwire is clean.
- **Implementer claim 1 (the teardown throw was a test bug).** Mechanism
verified. Both corrected assertions were genuinely wrong for the stated
reasons, and the `using`-finally exception-replacement is real. See A8 for
the residual.
- **Sabotage B's asymmetry.** Verified structurally:
`PortalProducerInvalidAuthority_ArmDoesNotRunAndNothingMutates` builds an
authority with `RevealGeneration: 0`, which fails
`RuntimePortalPlacementAuthority.IsValid` at
`TryExecuteAcceptedPortalArrival:459`**before** `ClassifyPortalArrival` is
reached. Forcing the classifier to reject cannot change that test's outcome,
so the 4-of-5 asymmetry is exactly what the code shape predicts. Good
evidence.
- **Register bookkeeping.** AD-42's deletion is justified (its last citation
was D2's two-call `Resolve`+`ResolvePlacement`, which is gone); AD-2's
amendment states the deferred-place adaptation, the T8 tolerance, and the
leash-anchor nuance as D-T9 required; the `:2276` stale comment correction
landed; the 2026-07-16 pseudocode `enter_world` correction landed. Row count
49→48 is consistent.
- **Build.** `dotnet build AcDream.slnx -c Debug` exit 0 at the reviewed tree.
---
## Judgment on the `_pendingDestination` lifetime
**The cached destination's lifetime is correct. I found no way to Place
against a superseded destination, and the Place-time re-read it replaced
protected against nothing.**
The reasoning, checked against source:
1. **The bug was real and total.** `RuntimeWorldTransitState
.TryBeginPortalReveal:159-183` clears `_hasAcceptedDestination` and
`_acceptedDestination` at `:180-181` on success.
`TryGetAcceptedTeleportDestination:522-527` returns `_teleportActive &&
_hasAcceptedDestination`. Since `AimDestination` drives
`TryBeginPortalReveal` through `WorldRevealCoordinator.TryBeginPortal`
(`:742`), the slot is empty at every Place edge. The old re-read could
never succeed — every real portal placement would have refused with
`cause=host-token-unavailable`. Not a stale-destination guard; a hard
failure.
2. **Write/clear is in exact lockstep with `_pendingCell`.**
`_pendingRotation`/`_pendingCell`/`_pendingDestination`/
`_hasPendingDestination` are written together at `:780-783` and cleared
together at `:801-805` in `ResetTransit`. `_pendingCell != 0u` is itself the
`haveDestination` predicate (`:481`), so the two cannot diverge.
`_pendingDestination.Position.ObjCellId` **is** `_pendingCell` by
construction (`Position position = destination.Position;` at `:715`).
3. **A second Aim cannot produce a mismatched pair.** Supersession by a new
F751 goes through `OnTeleportStarted``ResetTransit(clearSession: false)`,
which clears all four fields and bumps `_lifetimeGeneration`. Supersession
by a second destination on the *same* sequence is impossible:
`OfferTeleportDestination:490-497` returns `false` once `_destinationAccepted`
is set, and `TryBeginPortalReveal` clears only `_hasAcceptedDestination`,
leaving `_destinationAccepted` latched for the life of the reveal. So the
destination is pinned from Aim to terminal, by the transit itself.
4. **The one torn window fails closed.** `_pendingRevealGeneration` is written
at `:749`, before the `IsCurrentLifetime`/recenter guards at `:750`,
`:757`, `:764`, while the other three are written at `:780-783`. A `false`
return from any of those guards leaves a NEW generation paired with an OLD
cell/destination. Both Place-edge gates then refuse:
`CanPlacePortalDestination(newGen, seq, oldCell)` fails
`IsCurrentPortalDestination`'s `destinationCell == _snapshot.DestinationCell`
check, and `TryRegisterHostProjection(newGen, oldCell)` fails the same
comparison at `RuntimeWorldTransitState.cs:197-209`. Neither can commit a
stale pair. (This tearing predates the slice — `_pendingCell` already had
it; `_pendingDestination` does not worsen it.)
5. **Terminal clearing.** Commit → `FireLoginComplete``ResetTransit`.
Session reset / generation reset → `ResetSession` / `ResetGenerationPresentation`
`ResetTransit`. Cancellation through `ResetTransit(clearSession: false)`.
A *refused* Place leaves the fields set — but so does `_pendingCell`, and
both Place-edge gates are keyed on the reveal generation, so a retained
value is inert until a new Aim overwrites it or a reset clears it.
The one thing the caching genuinely costs is test coverage, not correctness:
A6 removed the only assertion that could have distinguished a stale cached
destination from a fresh one. Restore it.
---
## Summary
| # | Severity | Finding |
|---|---|---|
| A1 | MAJOR | Refused Place does not stop the anim stream; player released into the world unplaced. P4 undischarged. |
| A2 | MAJOR | `DeferredCell` park commits after `EndTeleport`: body/presentation split + unconsumable Place receipt (P3 graphical half, D-T2.4 re-validation missing). |
| A3 | MEDIUM | Headless discards the arm's status; `is not null` guard silently disables placement. |
| A4 | MEDIUM | `route.ZeroVelocity`/`ConstrainPhase`/`TeleportHookPhase` never read; inversions hardcoded; contract sabotage 3 cannot fail. |
| A5 | MEDIUM | §8 item 8 presentation suite missing; local-player shadow invariant unasserted, and a real cache/publish desync sits behind it. |
| A6 | MEDIUM | Superseded-teleport test lost its destination discriminator; two sibling tests weakened, one now contradicts its name. |
| A7 | MEDIUM | Dual-host parity test obligation (§8 item 6) not met; headless flip untested. |
| A8 | MINOR | Portal-test cleanup outside `finally` masks the first failure behind a Dispose throw. |
| A9 | MINOR | Two sources for the teleport sequence (transit vs destination). |
| A10 | MINOR | Class doc and plan correction each assert behaviour the code does not have. |
| A11 | MINOR | Process-global `LocalTeleportHostKind` (accepted, noted). |
| A12 | MINOR | Synthetic indoor cell is geometry-free — fine as a gate, weak as a placement oracle. |

View file

@ -0,0 +1,434 @@
# C4 route 3 — retail-conformance review, round 2 (delta) — 2026-08-05
**Verdict: FAIL — but a near miss.** All three round-1 MAJOR retail
findings (R1, R3, R8) are genuinely fixed, and the R1 fix is the right
shape for the right retail reason. What blocks is small and cheap: one
unsound "the park committed" inference that reopens R1's failure mode on
a narrow path (**N1**), one approximation shipped with a code comment
instead of the register row the project's binding rule requires
(**R7**), and one transient condition converted into a fatal exception on
the headless endurance path (**N3**).
**R4/A5 (the presentation suite) explicitly does NOT block** — see §D.
That call changed from round 1 because the A10 correction is true: I
verified the canonical Place receipt, not the suffix, is the render
entity's mover, and both halves of that path already have tests.
Scope: delta against my round-1 report
(`2026-08-04-c4-route-3-retail-review.md`). Same working tree, HEAD
`cd3129e9`, uncommitted. Review only.
---
## §A — round-1 findings: disposition
| # | round-1 finding | round-2 status |
|---|---|---|
| R1 | `Place` one-shot; no re-attempt driver; reveal completes with an unplaced body | **FIXED** — §B, and the fix is retail-correct |
| R2 | headless discards the arm's status, acks a materialization that never happened | **FIXED** — §B.3 |
| R3 | probe's `leash` field can never read `armed` | **FIXED**`Constraint?.IsConstrained` at `RuntimeAcceptedPositionDriveController.cs:838` |
| R4 | App presentation suite missing; assertions weakened | **PARTIALLY closed, does not block** — §D |
| R5 | headless dual-host parity untested | **FIXED**`HeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake` |
| R6 | `enter_world` caller-sweep correction mis-stated the retail record | **FIXED and independently re-verified** — §C.4 |
| R7 | retail's movement refresh is autonomy-gated; acdream's was not | **PARTIALLY fixed — BLOCKS on register discipline** — §C.3 |
| R8 | probe fired only on commit; refusals invisible under the pinned env var | **MOSTLY fixed** — two App-side causes remain invisible, §E.2 |
| R9 | stale "TryBeginPortal (below)" | **FIXED** (`:751` now reads "above") |
| R10 | `AD-131` does not exist | **FIXED**`AP-131` |
| R11 | probe hardcoded `autorunCancelled: true` | **FIXED** — reads `CancelAutoRun()`'s bool |
---
## §B — the R1 fix: correct, and correct for the right retail reason
`LocalPlayerTeleportController.cs:527`
```csharp
bool placementReady = dataReady && TryAdvancePortalCommit(sequence);
...
var (_, events) = _presentation.Tick(deltaSeconds, placementReady);
```
Inverting the readiness feed instead of touching the sequencer is the
right call, for a reason worth recording: **it is retail's own shape.**
Retail holds the player in portal space on `CellManager::blocking_for_cells`
and `SmartBox::UseTime` @0x00455410 runs only `CheckPrefetchStatus` until
the destination is usable — the hold lives in the *readiness predicate*,
not in the animation. Feeding the sequencer "the canonical commit has
already happened" rather than "the data is ready" reproduces that hold
without changing a single sequencer timing (stop condition 2 respected).
`TeleportAnimSequencer.cs` is untouched — confirmed by `git diff`.
### B.1 — arrival ORDER is preserved; Inversion B intact
Verified by tracing the single tick on which the commit succeeds:
1. `TryAdvancePortalCommit``TryExecuteAcceptedPortalArrival`
`TryPrepareAndSubmitAuthoredPlacement` → **canonical body commit**
(`CommitCanonical`, retail `SetPositionSimple` @0x005162B0).
2. `ReconcileAndAcknowledgePortal``CommitCanonicalTeleportFrame`
(UnStick @0x00514EEE / UnConstrain @0x00514F02 / re-arm @0x0045418A,
velocity zero @0x004541B4, StopCompletely) — **after** the placement.
3. `CancelAutoRun()` + movement refresh — the `PlayerTeleported`
@0x006B32B0 port, **after** the hook tail.
4. Only then does the sequencer see `worldReady=true`, emit `Place`, and
run the presentation suffix (`NotifyTeleported`, camera reset,
reconcile) and `ObserveMaterialized`.
Retail: `SetPositionSimple` @0x00453924`PlayerPositionUpdated`
@0x00453932`teleport_hook` @0x004538AE`PlayerTeleported`
@0x004538B3`set_viewer` @0x004538D5. **Same order.** Inversion B
(local hook AFTER placement) holds; the gating did not move it.
Two sub-order deltas versus retail, both traced and both **unobservable**
— stated so a future reader does not re-derive them:
- Retail's `ConstrainTo` @0x0045418A and `set_velocity` @0x004541B4 run
in `HandleReceivedPosition` *after* `PlayerPositionUpdated` returns,
i.e. after `SendMovementEvent` and `set_viewer`. acdream runs both
inside `CommitCanonicalTeleportFrame`, before them. Nothing reads the
leash between those points, and `MoveToStatePack` @0x006B4720 packs
`InqRawMotionState` + `m_position` + contact + longjump + timestamps —
**no velocity** — so the outbound bytes are identical either way.
- `NotifyTeleported()` (teleport_hook's TargetManager teardown) now runs
in the presentation suffix, i.e. after `PlayerTeleported`, where retail
runs the whole hook before it. Purely local; no interaction with the
outbound send.
### B.2 — the refusal path is now genuinely held, and tested with teeth
`RefusedPlace_HoldsTheStreamAndConvergesOnlyAfterContentionClears`
(App.Tests) forces a real Runtime `Contention` by taking the entity's
placement token, drives **100 ticks at 0.1 s** — 10 s, roughly 3× the
tunnel's own 25 s timing, well past where the pre-fix code fired
`FireLoginComplete` — and asserts the positive facts: body unmoved,
`IsActive`, `Snapshot.Completed == false`, `LoginCompleteCount == 0`.
It then releases the competing operation and asserts the very next tick
commits at the offered destination. That is a real discriminator, not a
negative-only assertion, and it directly kills R1.
A permanent refusal now holds in the tunnel showing retail's centered
wait cue (`_holdSeconds` accumulates on `!placementReady`) until
supersession or session reset — which is exactly what contract D-T5
pinned and §4 item 4 requires.
### B.3 — headless
`PrepareDestination` now throws if no drive is wired, gates the attempt
on `_collision.IsReady(destination.CellId)`, and returns
`IsCollisionReady: committed`;
`RuntimeLiveEntitySessionController.TryAdvancePortalCompletion` returns
early on `!IsCollisionReady`, retried by `PumpPortalCompletion` from
`HeadlessSessionHost.Tick`. The materialization ack can no longer
precede a placement. R2 closed.
### B.4 — P3 (no wedgeable portal receipt) is now ESTABLISHED
Neither review had closed this. `RuntimePlacementProjectionSubscription.OnPlacement`
(`:122-150`) calls `_sink.TryApply(in head)` **synchronously on publish**,
so a portal-carrying Place receipt is applied and acknowledged inside the
commit call, while the transit is still active and
`IsCurrentPlacementAuthority` still holds. This matters most on headless,
where `TryAdvancePortalCompletion` runs `Complete` + `EndTeleport`
synchronously right after the commit and the FIFO is only republished at
the end of the same `Tick` — had delivery been deferred, that receipt
would have failed the portal gate forever and wedged the placement FIFO
for every entity. It does not. **P3 satisfied on both hosts.**
---
## §C — retail verification of the round-2 changes
### C.1 — A4: the route fields carry retail semantics (with one granularity defect)
Verified in `RuntimeAuthoritativePositionRouteClassifier.cs:164-171`:
- `ConstrainAfterRouting => ConstrainPhase is AfterPositionOperation`
**correct and discriminating.** It separates the three real retail
cases: force (`None`, early return @0x0045409D), local teleport
(`After`, `ConstrainTo` @0x0045418A), local non-teleport (`Before`,
`ConstrainTo` @0x004541EC). The sabotage the contract's §8 item 11
demanded can now fail as designed.
- `ZeroVelocity` — read directly; retail `set_velocity` @0x004541B4.
**N4 (LOW) — `RunsTeleportHook` gates too much.**
`RuntimeAcceptedPositionDriveController.cs:795-801` uses
`route.RunsTeleportHook` (`TeleportHookPhase is not None`) to gate the
**entire** `CommitCanonicalTeleportFrame` call. But that method does far
more than retail's `teleport_hook` @0x00514ED0 (CancelMoveTo, UnStick,
StopInterpolating, UnConstrain, TargetManager, report_collision_end): it
also resets the render-lerp anchors, publishes `UpdateCellId`, runs
StopCompletely, resets the input edges, and resets the object clock —
none of which retail conditions on the hook. Retail's `SetPositionInternal`
@0x00515330 does the frame/cell work unconditionally. Today the portal
route always sets the phase, so there is no live effect; but a future
`TeleportHookPhase.None` would silently skip the render-root cell publish
— the doorway-FLAP class. Gate only the `UnStick`/`UnConstrain`/re-arm
block on the hook phase.
`RunsTeleportHook` also collapses `Before` and `After` into one boolean.
Harmless here because the call site is unconditionally post-commit, but
it means the field cannot express Inversion B by itself.
### C.2 — the "sequence" plumbing
`TryExecuteCanonicalPortalPlacementCore` now takes the authority's
`TeleportSequence` from `_pendingDestination` and `Debug.Assert`s it
equals the transit's `ActiveTeleportSequence`. Sound: the two can only
diverge through a bug, and `RuntimeWorldTransitState.OfferTeleportDestination:495-503`
accepts **exactly one** destination per active sequence
(`if (_destinationAccepted) return false;`), so the Aim-time snapshot is
unique per sequence by construction. (This also disposes of a hazard I
went looking for: a second destination cannot supersede within a
sequence, and a new sequence routes through `OnTeleportStarted`
`ResetTransit`, which clears `_placementCommitted`/`_awaitingDeferredWake`
at `:932`.)
### C.3 — R7: the autonomy gate — **the approximation is real and needs its register row (BLOCKING)**
The retail reading is now exactly right, and I verified both halves:
- `CommandInterpreter::UsePositionFromServer` @0x006B3B40 (pseudo-C
`:699506-699510`) is literally `return this->autonomy_level != 2`.
acdream's `RuntimeCharacterState.UsePositionFromServer` (`:122`) is
`AutonomyLevel != FullAutonomyLevel(2)` — **an exact port.**
- `CommandInterpreter::SendMovementEvent` @0x006B4680 gates on
`this->autonomy_level != 0` (@0x006B46BB, pseudo-C `:700283`).
So `!UsePositionFromServer` sends at level 2 only; retail sends at levels
1 **and** 2. The implementer's characterisation is accurate, and the
divergence is currently unreachable — `TrySetAutonomyLevel` has **zero
production callers** (only `RuntimeCharacterStateTests`), so
`AutonomyLevel` is always 2 and the two gates agree. Retail's own default
is 2 (`command_line_autonomy_level = 0x2`, pseudo-C `:1088429`).
That is precisely what an approximation is: correct today, wrong the day
someone wires level 1. CLAUDE.md's register rule is binding and admits no
implementer discretion — *"Any commit that introduces a deviation (an
adaptation, an approximation, a stopgap, a 'retail does X but we…') adds
its register row IN THE SAME COMMIT. … A deviation found without a row is
a bug twice over."* The shipped disposition is a code comment at
`RuntimeAcceptedPositionDriveController.cs:812-824` saying *"not
register-worthy on its own (no user-visible#-labeled symptom yet)"*. A
symptom is not the threshold; a deviation is. Either thread the raw
`AutonomyLevel` through (the exact port, and the constructor already
takes two optional funcs so the marginal cost is one more) or add the
row. A comment is not the register.
### C.4 — R6: the corrected `enter_world` banner is now accurate
Re-verified independently against the pseudo-C, not merely re-read:
- `:93797` @0x004550EC and `:93824` @0x00455095 both sit inside
**`SmartBox::HandleCreateObject` @0x00454C80**. ✓
- `CObjectMaint::CreateObject` is invoked at @0x00454FD8 *inside* that
function — a callee, not the enclosing scope. ✓
- @0x004550EC is in the `if (arg3 != this->player_id)` **non-player**
branch; only @0x00455095 follows `SmartBox::init_player` +
`CellManager::ChangePosition` in the player branch. ✓
- The load-bearing negative still holds: the `Position*` overload
@0x00516310 has exactly those two callers and the `int` overload
@0x00516170 is reached only from @0x00516327`enter_world` is not on
the portal path.
The banner now states all four facts correctly. This citation is safe for
future sessions to cite.
---
## §D — R4/A5: still open, and it does NOT block. Here is why the call changed.
Round 1 rated this MAJOR on the premise (taken from contract §3.4) that
the sink *"writes no pose"*, making the suffix the render entity's only
mover. **That premise was wrong, and the A10 correction is right.** I
verified `LiveEntityRuntime.TryApplyRuntimePlacementPlace`
(`LiveEntityRuntime.cs:1386-1420`): a `Place` receipt calls
`entity.SetPosition(projection.WorldPosition)`, sets `entity.Rotation`,
sets `entity.ParentCellId = token.ExactCellId`, and calls
`_spatial.RebucketLiveEntity``commitPose` defaults true and is passed
`false` only for `WithdrawalRestored` (`:1364-1372`). The canonical
receipt IS the mover; the suffix's writes are redundant repeats, exactly
as the rewritten class comment now says. The comment is verified true.
With that established, the coverage picture is materially different from
round 1:
- the sink's Place-receipt render-entity reframe + rebucket —
**covered** (`RuntimePlacementPresentationSinkTests.Place_ReframesAndRebucketsExactSidecarWithoutMutatingRuntimePhysics`);
- the sink's **portal gate discriminating** — **covered**
(`PortalPlace_RequiresExactCurrentTransitHostAndSequence`, which drives
a valid authority and a mismatched sequence);
- the suffix's own entity write + destination bucket ordering —
**covered** (the two `ConcretePlacement_*` tests, adapted to the new
signature against a real `LiveEntityRuntime`/`WorldEntity`/`GpuWorldState`);
- the **canonical body** pose and cell after an App-driven portal commit
**now covered**: the four weakened assertions were restored against
`harness.Movement.Controller.Position`/`.CellId`, including A6's
superseded-destination discriminator, and the App harness now runs a
real Runtime rather than a fake placement.
What remains uncovered is narrower than "the presentation suite was not
built": no single test composes teleport-controller → canonical commit →
real sink → suffix; the **local-player collision shadow** after a portal
commit (#312's own layer) is unasserted; the T8 overwrite ordering is
unasserted; and the refused-Place test asserts body/LoginComplete/IsActive
but not the render entity.
That is a genuine gap and it is #312-adjacent, so it must be recorded and
closed in C5 — the implementer flagged it explicitly rather than claiming
closure, which is the right behaviour. But it is no longer a
"nothing asserts the only mover" hole, and it does not block this slice.
---
## §E — new findings
### N1 — MEDIUM — `PendingCount == 0` does not mean "the portal committed"; R1's failure mode survives on a narrow path
`LocalPlayerTeleportController.cs:653-657`
```csharp
if (_awaitingDeferredWake)
{
if (_acceptedPositionDrive.PendingCount != 0)
return false;
_awaitingDeferredWake = false;
_placementCommitted = true; // ← inference
return true;
}
```
and identically `HeadlessSessionWorldProjection.cs:818`
(`committed = _acceptedPositionDrive.PendingCount == 0;`).
`PendingCount` is `_pending is null ? 0 : 1`
(`RuntimeAcceptedPositionDriveController.cs:330`) — a shared, arm-agnostic
counter. Three `Advance` paths clear a **portal** pending, and only one
of them commits:
| `Advance` site | commits? | logs? |
|---|---|---|
| `:918` completed-wake, authority current | yes → `ReconcileAndAcknowledgePortal` | yes |
| `:918` completed-wake, authority stale | body committed by `RetryDeferred`, **suffix skipped** | `AbandonedAtWake` |
| `:973` **watch died** — "most likely a subsequent accepted Position's merge-time Forget" | **no** | **no line at all** |
| `:1020` `!IsPlacementCurrent` on the prepare-retry | **no** | **no line at all** |
On the last two the caller latches `_placementCommitted = true` for a
placement that never happened: the sequencer is released, `Place` fires,
`ObserveMaterialized` acks a materialization that did not occur, and the
player is revealed at the **origin**. That is exactly R1's shape,
re-entered through the deferred door.
Reachability is genuinely low — both hosts now gate the attempt behind
collision readiness (`_worldReveal.Evaluate(...).IsReady` graphically,
`_collision.IsReady(...)` headless), so a park is rare — and the
Runtime-layer test `PortalDeferredCell_WakeAbandonsInsteadOfReconcilingWhenAuthorityWentStale`
proves at least one non-committing convergence exists
(`Assert.Equal(0, drive.PendingCount)` with no reconcile). Nothing tests
the caller's inference against it.
The fix is small: have the drive report a portal-specific terminal
outcome (it already distinguishes them well enough to log
`AbandonedAtWake`) instead of the caller inferring commit from a shared
counter. Also give the two silent branches a probe line — under D-T8 they
are portal-arrival attempts that ended.
### N2 — LOW/MEDIUM — headless `IsUnhydratable` is now hardcoded false
`HeadlessSessionWorldProjection.cs:874` reports `IsUnhydratable: false`
unconditionally, where it previously reported `!ready`. The old value was
itself a conflation (not-yet-resident ≠ unhydratable), so this is not a
regression in meaning — but headless can now never report an
unhydratable destination, so a genuinely unhydratable claim spins in
`PumpPortalCompletion` forever instead of taking AD-2's stated "loud
unhydratable-placement path". Either derive the real predicate or state
in the method's doc that headless does not model it.
### N3 — MEDIUM — a transient headless condition is now fatal
`HeadlessSessionWorldProjection.cs:854` throws `InvalidOperationException`
on the `default:` arm, which covers **`NotApplicable`** as well as
`Rejected`. `NotApplicable` is returned by
`TryExecuteAcceptedPortalArrival` for `record.PhysicsBody is null` and
for an active initial-Create residence
(`RuntimeAcceptedPositionDriveController.cs:243-251`) — hydration-race
conditions, not "the reveal is stale". The exception message asserts a
diagnosis ("the reveal itself is stale or the local player has no
canonical body, neither recoverable by waiting") that is true for
`Rejected` and not established for `NotApplicable`.
Refusing to fake success is right; converting a possibly-transient
condition into a process-killing throw on the host that must survive
K4's 30-session / two-hour endurance profile is the wrong end of that
trade. Split the arm: throw on `Rejected`, treat `NotApplicable` as a
retryable wait with a bounded attempt budget (or a loud log plus
`IsCollisionReady: false`).
### N4 — LOW — `RunsTeleportHook` over-gates the frame commit (§C.1)
### N5 — LOW — the plan-doc correction contradicts itself
`docs/plans/2026-08-02-placement-cutover.md` now opens *"This line was
accurate for both halves at the time it was written"*, explains that
route 3 added the consumer and producer together, and then closes with
*"'Zero producing call sites; the adapter does not exist' was accurate
only for the producer half"* — which contradicts the opening sentence.
The substance is right (the type, `IsValid`, `Pending.Portal`, the sinks'
gates and `BeginAcceptedPlacementCore`'s gate pre-dated route 3; the arm
did not); the paragraph needs one pass so a future reader can cite it.
---
## §E.2 — R8 residual
`LogPortalArrivalAttempt` now fires on every Runtime-side exit under the
gate's own `ACDREAM_PROBE_LOCAL_TELEPORT`, and a live `Contention`
refusal was observed — R8's substance is closed and **§9's gate is now
passable as specified**: `leash=armed` reads from
`ConstraintManager.IsConstrained`, which `ConstrainTo` @0x00556240 sets
unconditionally, so a committed arrival prints `armed`.
Two App-side refusal causes still never reach Runtime and therefore emit
no `[local-tp]` line: `cause=stale-reveal`
(`LocalPlayerTeleportController.cs:666`, the `CanPlacePortalDestination`
preflight) and `cause=host-token-unavailable` (`:737`). Both log through
`PhysicsDiagnostics.LogTeleport`, gated by the *different*
`ACDREAM_PROBE_TELEPORT`. Under the pinned gate environment these are
invisible. Either route them through `LogLocalTeleportArrival` or add
`ACDREAM_PROBE_TELEPORT=1` to §9's environment line.
---
## §F — gate evidence
- **Release build**: green, 0 warnings / 0 errors.
- **`AcDream.Runtime.Tests`**: 1,164 passed / 0 failed / **0 skipped**.
- **`AcDream.Headless.Tests`**: 85 passed / 0 failed / **0 skipped**.
- **`LocalPlayerTeleportControllerTests`**: 21 passed / 0 failed / **0 skipped**.
- No `Skip` attribute in any touched test file. No test weakened: the
four round-1 weakenings were restored with stronger targets (the real
canonical body rather than the deleted fake's captured argument), and
three genuinely new discriminators were added (`RefusedPlace_…`,
`PortalCommitted_UnderServerControlSendsNoMovementEvent`,
`PortalDeferredCell_WakeAbandonsInsteadOfReconcilingWhenAuthorityWentStale`,
plus the headless park test).
- Per process rule 3, none of this is evidence of correctness — it is
evidence that nothing regressed while the above holes remain.
---
## §G — what must land to pass
1. **N1** — stop inferring commit from `PendingCount`; report a
portal-specific terminal outcome, and log the two silent
non-committing `Advance` branches.
2. **R7** — add the AD register row for the autonomy approximation, or
thread the raw `AutonomyLevel` through and make it exact. Binding
project rule; not an implementer judgment call.
3. **N3** — do not throw on `NotApplicable`; split it from `Rejected`.
Cheap follow-ups, non-blocking: N2, N4 (gate only the UnStick/UnConstrain/
re-arm block on `RunsTeleportHook`), N5, and R8's two App-side causes.
Record as a dated, named C5 item: R4/A5's residual — the end-to-end
composition test, the local-player collision shadow after a portal
commit, and the T8 overwrite ordering.

View file

@ -0,0 +1,515 @@
# C4 route 3 — retail-conformance review (2026-08-04)
**Verdict: FAIL.**
Reviewer scope: the uncommitted working-tree diff at HEAD `cd3129e9`
(`git diff HEAD` + untracked), branch
`claude/acdream-physics-divergence-5aa784`. Review only — no edits made.
The retail *reading* in this slice is excellent. Every §1 claim in the
pinned contract reproduces line-for-line in
`docs/research/named-retail/acclient_2013_pseudo_c.txt` (§A below), both
inversions are implemented in the right direction, the D-T3 duty map is
complete, and the implementer's P1 finding on TransientState is not just
correct — it retires a real pre-existing divergence.
The slice fails on **what happens when the placement does not commit**.
`TeleportAnimEvent.Place` is one-shot, so every non-`Committed` outcome
silently skips the placement, the presentation suffix, and the
materialization acknowledgement while the animation stream marches on to
reveal the world and fire LoginComplete anyway. The headless host has the
same hole with the extra property that it *asserts a materialization that
did not happen*. Neither is covered by a test, because the App-layer
presentation suite the contract made mandatory (§8 items 8/9/10, closing
route 2's B2 gap) was not written — the existing App assertions were
weakened instead. And the one probe field the connected gate keys on
(`leash=armed`) can never be true as coded.
---
## Findings
### R1 — MAJOR — `TeleportAnimEvent.Place` is one-shot; there is no re-attempt driver, and the reveal completes anyway
`src/AcDream.Core/World/TeleportAnimSequencer.cs:136-141`
```csharp
case TeleportAnimState.Tunnel:
if (worldReady)
{
evts.Add(TeleportAnimEvent.Place);
Advance(TeleportAnimState.TunnelContinue, enterTunnel: false);
```
The state advances in the **same tick** the event is emitted. `Place`
never fires again for that reveal.
`src/AcDream.App/Streaming/LocalPlayerTeleportController.cs:505-522`
```csharp
case TeleportAnimEvent.Place:
if (!_worldReveal.CanPlacePortalDestination(...)) return;
if (!TryExecuteCanonicalPortalPlacement(sequence)) return; // :513
...
_placement.Place(_pendingRotation); // :517
...
_worldReveal.ObserveMaterialized(...); // :520
```
`TryExecuteCanonicalPortalPlacement` returns `true` **only** on
`RuntimeAcceptedPositionExecutionStatus.Committed`
(`LocalPlayerTeleportController.cs:597-598`). Every other status —
`Contention`, `Rejected`, `NotApplicable`, and notably `DeferredCell`
returns `false` and the `Tick` returns.
Consequences, all reachable:
- `_placement.Place` never runs → no `entity.SetPosition` /
`ParentCellId` / `RebucketLiveEntity`, no `NotifyTeleported()`, no
camera reset, no `_spatial.Reconcile()`.
- `_worldReveal.ObserveMaterialized` never runs →
`RuntimeWorldTransitState.AcknowledgePortalMaterialized` never fires →
`Materialized` stays false.
- The **next** tick still advances the sequencer:
`TunnelContinue``TunnelFadeOut``PlayExitSound`
(`RevealWorldViewport`) → `WorldFadeIn``FireLoginComplete`
(`_mode.EnterWorld()` + `SendLoginComplete()` + `_worldReveal.Complete()`
+ `ResetTransit`).
- `RuntimeWorldTransitState.Complete` then trips
`FailInvariant("portal-complete-before-materialized")`
(`RuntimeWorldTransitState.cs:701-706`) and returns `false`;
`WorldRevealCoordinator.Complete()` (`:268-276`) **discards** that
`false`. `ResetTransit(clearSession:false)` then calls
`_transit.EndTeleport()` + `_worldReveal.Cancel()`, so the ledger
converges — but the reveal is recorded cancelled, not completed, with
one invariant failure logged.
User-visible outcome on `Contention`/`Rejected`: the player is revealed
into the destination world **standing at the origin position**, with
LoginComplete sent. On `DeferredCell`: the body commits later at the
collision-generation wake (`Advance``ReconcileAndAcknowledgePortal`),
but the presentation suffix, camera reset, rebucket, and materialization
ack are gone forever.
This is exactly what contract §4 items 4 and 5 forbid ("a refused
placement must NOT … must not advance the anim-event stream's terminal
events"; "never a half-state", "never a silent wedge in portal space")
and it is the D-T5/P4 obligation the contract flagged in advance: *"if
the Place anim event is one-shot, the re-attempt must be driven by the
same Tick predicate that produced it, and THAT mechanism must be stated
in the commit."* It is one-shot, and no mechanism was added.
The comment shipped in its place is false. `LocalPlayerTeleportController.cs:566-572`:
> "On any refusal this returns `false` without mutating anything — the
> D-T5 refusal shape: … the transit's own cancellation/supersession
> machinery is the authority on what happens next."
The transit is not the authority on what happens next. The animation
sequencer is, and it does not wait.
**Correct behaviour:** either re-drive the Place edge from the same
`ready`-gated Tick predicate until it commits (holding the sequencer in
`Tunnel` — which is what retail's `blocking_for_cells` hold is), or
cancel the reveal explicitly on refusal so the player is never revealed
without a committed placement. Retail has no third option: it places
unconditionally and immediately (`SmartBox::TeleportPlayer` @0x00453910)
and only the *simulation* waits on prefetch.
---
### R2 — MAJOR — headless discards the arm's status and acknowledges a materialization that did not happen
`src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs:775`
```csharp
_ = _acceptedPositionDrive.TryExecuteAcceptedPortalArrival(
destination,
authority);
```
The status is dropped on the floor. `PrepareDestination` then
unconditionally returns a ready readiness report, and
`RuntimeLiveEntitySessionController.TryCompletePortal` continues its
fully-synchronous suffix: `AcknowledgeDestinationReadiness`
`AcknowledgePortalMaterialized``SimulationReleaseProjected`
`Complete``SendGameAction(LoginComplete)``EndTeleport`.
So on any refusal or park, the headless host **fires
`AcknowledgePortalMaterialized` for a placement that never committed** —
contract §4 item 5 and D-T5 rows 1/2 both state in terms that the
materialization ack must fire only from the committed outcome. The bot
reports a completed teleport while standing where it started, with no
log line of any kind (see R8).
Retail contradiction is indirect but real: retail's
`SmartBox::PlayerPositionUpdated` @0x00453870 clears
`waiting_for_teleport` **inside the same call that performed
`SetPositionSimple`** (@0x00453924@0x0045389A). The "wait is over"
edge is downstream of the placement in retail; here it can precede a
placement that never occurred.
---
### R3 — MAJOR — the D-T8 probe's `leash` field can never read `armed`; the connected gate as pinned is unpassable
`src/AcDream.Runtime/Session/RuntimeAcceptedPositionDriveController.cs:694`
```csharp
leashArmed: controller.PositionManager?.IsFullyConstrained() ?? false,
```
Retail `ConstraintManager::IsFullyConstrained` @0x005560D0 is
`constraint_distance_max * 0.9 < constraint_pos_offset` — "has strained
past 90 % of the leash", the predicate `jump_is_allowed` reads. It is not
"is the leash armed". The acdream port says so explicitly
(`src/AcDream.Core/Physics/Motion/ConstraintManager.cs:79-89`).
Retail `ConstraintManager::ConstrainTo` @0x00556240 (pseudo-C
:353528-353537) ends with
`constraint_pos_offset = Position::distance(anchor, physics_obj->m_position)`;
acdream mirrors it at `ConstraintManager.cs:65-71`. Because
`RearmConstraintLeashAtCurrentPosition`
(`PlayerMovementController.cs:1836-1845`) anchors at the body's **own**
`CellPosition`, that distance is 0. `max * 0.9 < 0` is false.
Therefore every committed portal arrival prints `leash=unarmed`. The
contract's §9 pass criterion — *"Pass requires ALL of … `leash=armed`"*
cannot be met, and a future reader hitting `leash=unarmed` would chase a
phantom missing leash (the exact 4b-3 A1 defect class the contract
warned about, inverted).
The correct observable is `ConstraintManager.IsConstrained`, which the
Runtime test itself uses
(`RuntimeAcceptedPositionDriveControllerTests`,
`Assert.True(controller.PositionManager.Constraint!.IsConstrained)`).
`PositionManager` does not currently surface it; it needs to.
---
### R4 — MAJOR — the mandatory App-layer presentation suite is missing, and the existing App assertions were weakened
`tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs`
gained **zero** new `[Fact]`s. All five new tests in the diff are in
`tests/AcDream.Runtime.Tests/Session/RuntimeAcceptedPositionDriveControllerTests.cs`.
What changed in the App file is assertion *strength*, downward:
```diff
- Assert.Equal(new Vector3(7f, 8f, 9f), harness.Placement.Position);
+ Assert.True(harness.Placement.Called);
```
(and the same substitution at eight further sites). The interface change
makes the literal old assertion impossible, which is fine — but the
contract required the replacement coverage and named it as load-bearing:
- §8 item 8: "after a committed portal placement through the REAL sink +
suffix, the render `WorldEntity` position/rotation/`ParentCellId` equal
the resolved body, the draw bucket moved, the local-player shadow
agrees, and the sink's Place receipt was consumed with a VALID portal
authority … **route 2's B2 coverage gap … becomes load-bearing here and
MUST close**."
- §8 item 9: the T8 overwrite ordering (wire pose then resolved pose).
- §8 item 10: refused Place edge presentation.
None exist. Since `RuntimePlacementPresentationSink.TryPublishPlace`
writes no pose (contract §3.4, re-confirmed), the suffix in
`LocalPlayerTeleportPlacement.Place` is now the render entity's **only**
mover — and nothing asserts it moves the entity to the resolved pose.
Combined with R1 (where that suffix is skipped entirely on refusal), this
is the #312 shape verbatim: process rule 4, "tests must assert the layer
that broke."
---
### R5 — MAJOR — headless dual-host parity (§8 item 6, D-T6) has no coverage, self-declared open
`tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs:413-422`
(added in this diff):
> "no accepted-position drive controller is wired into this fixture's
> projection … so the canonical portal arm this method now calls is a
> no-op here by construction (`_acceptedPositionDrive` is null) … a
> headless-host-specific committed-portal test is an open item, not
> attempted here given this session's time budget."
D2 (`ResynchronizeLocalPlayerForPortalArrival`, ~40 non-comment lines and
AD-42's last citation) was deleted and its replacement has **zero**
headless test coverage. D-T6 pinned this: *"dual-host parity is a test
obligation, not an aspiration."* The honest disclosure is appreciated and
does not change the finding.
Related, and unremarked in the diff: the deleted method also performed
`controller.LocalEntityId = record.LocalEntityId ?? 0u`. Verified safe —
`RuntimeLocalPlayerPhysicsPublicationState.cs:214` sets it at publication
and the entity key is stable across a portal — but the drop deserves a
line in the commit message.
---
### R6 — MEDIUM — the `enter_world` caller-sweep correction mis-states the retail record it is correcting
`docs/research/2026-07-16-portal-completion-pseudocode.md` §2.1 banner:
> "a caller sweep … shows both `CPhysicsObj::enter_world` call sites
> living inside `CObjectMaint::CreateObject`'s player branch
> (`SmartBox::init_player` + `CellManager::ChangePosition` immediately
> precede it)"
Verified independently. The two call sites are pseudo-C :93797
(@0x004550EC) and :93824 (@0x00455095). Both live inside
**`SmartBox::HandleCreateObject` @0x00454C80**, not
`CObjectMaint::CreateObject` — the latter is merely a *callee* at
@0x00454FD8 inside that same function. And they are **not both in the
player branch**: @0x004550EC is in the `if (arg3 != this->player_id)`
NON-player branch (`PhysicsDesc::get_position``enter_world(var_bc, …)`
for a newly created remote object); only @0x00455095 sits in the player
branch after `init_player` + `ChangePosition`.
The load-bearing NEGATIVE is **CONFIRMED**: the `Position*` overload
@0x00516310 has exactly those two callers, the `int` overload @0x00516170
is reached only from @0x00516327, and neither is on the portal path.
`SmartBox::TeleportPlayer``SetPositionSimple` is correct.
But this banner is explicitly a correction to the retail record that
future sessions will cite, and it is wrong in two of its three factual
clauses. Same defect class as "a register row asserting behaviour the
code does not have," applied to a research doc — the exact reason the
contract ordered the correction in-slice.
---
### R7 — MEDIUM — retail's post-teleport movement refresh is autonomy-gated; acdream's is not
Contract open question (c), answered.
`CommandInterpreter::SendMovementEvent` @0x006B4680 (pseudo-C
:700274-700313):
```
if ((player != 0 && this->smartbox != 0) && CPhysicsObj::InqRawMotionState(player) != 0)
if (this->autonomy_level != 0)
MoveToStatePack::MoveToStatePack(...)
SendMoveToStateEvent(...)
```
Two gates: a non-null raw motion state, and **`autonomy_level != 0`**.
Under server control retail sends nothing.
`RuntimeAcceptedPositionDriveController.cs:678-681` calls
`_localPlayerOutbound.TrySendMovement(...)` unconditionally;
`LocalPlayerOutboundController.TrySendMovement:187-229` gates only on a
resolvable outbound position. The controller already holds
`UsePositionFromServer` (retail's `UsePositionFromServer()`), and
`_usePositionFromServer` is already a field on this very class — the
autonomy fact is in scope.
Everything else about the port checks out: the message family is
`MoveToState` (retail packs `InqRawMotionState` into `MoveToStatePack`),
the contact byte is `Contact && OnWalkable` in both, exactly one is sent,
and no `AutonomousPosition` goes out (retail's teleport branch returns
before `SendPositionEvent` — verified at @0x004541C0). Order is right:
`CommitCanonicalTeleportFrame` (hook tail) → `CancelAutoRun`
movement send, matching @0x004538AE@0x004538B3 → tail-jump.
Either add the autonomy gate or file the delta as a register row.
---
### R8 — MEDIUM — D-T8 emits one line per *committed* arrival, not per attempt; refusals are invisible under the pinned gate environment
`PhysicsDiagnostics.LogLocalTeleportArrival` is called from exactly one
site, `ReconcileAndAcknowledgePortal`
(`RuntimeAcceptedPositionDriveController.cs:687-696`), reached only on
`CommittedHostAcknowledgementPending`. Its `placementStatus` argument is
the literal `"Committed"`.
The graphical refusal path logs via `PhysicsDiagnostics.LogTeleport`
(`LocalPlayerTeleportController.cs:582-583`, `:598-599`), which is gated
by **`ACDREAM_PROBE_TELEPORT`** (`PhysicsDiagnostics.cs:1160-1161`) — a
different env var from the `ACDREAM_PROBE_LOCAL_TELEPORT` the gate pins.
The headless refusal path logs nothing at all (R2).
Net: with the contract's pinned gate environment, a refusal produces zero
output on either host. D-T8 specified "One line per portal-arrival
attempt: cause … placement status", and §9 requires "zero
`Refused`/`Contention` lines in ordinary play" — unobservable as built.
Given R1, an unobserved refusal is precisely the failure that would ship.
---
### R9 — LOW — stale directional reference in a comment added by this diff
`src/AcDream.App/Streaming/LocalPlayerTeleportController.cs:775`:
"TryBeginPortal (below) drives …". `_worldReveal.TryBeginPortal` is
called **above** this comment, at `:741`, in the same method. Process
rule 6.
### R10 — LOW — `AD-131` does not exist
`src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2292`
cites "AD-2/AD-131/#275". The AD section has 48 rows. The row is
**AP-131** (`docs/architecture/retail-divergence-register.md:283`), which
is what the contract itself says. Introduced by this diff, in the very
comment the slice rewrote to fix a stale comment.
### R11 — LOW — the probe asserts more than it observes
`RuntimeAcceptedPositionDriveController.cs:693-695` hardcodes
`hookTailRan: true` and `autorunCancelled: true`.
`RuntimeLocalPlayerMovementState.CancelAutoRun():226-234` returns `false`
when autorun was already off (correctly mirroring retail's
`SetAutoRun` @0x006B4850, which acts only on a state *change* at
@0x006B4871). The field reports the action ran, not the state changed —
report the returned bool.
---
## §A — retail claims verified independently (do not re-derive)
All against `docs/research/named-retail/acclient_2013_pseudo_c.txt`.
| claim | where | result |
|---|---|---|
| `SmartBox::TeleportPlayer` @0x00453910 = `SetPositionSimple(player, dest, 1)` @0x00453924 + `PlayerPositionUpdated(this, 1, FLT_MAX)` @0x00453932, nothing else | :92514-92523 | **CONFIRMED** — the generic path, route 2's exact primitive, third route running |
| `PlayerPositionUpdated` teleport arm order: `position_update_complete=0` @0x00453890, `waiting_for_teleport=0` @0x0045389A, `has_been_teleported=0` @0x004538A4, `teleport_hook` @0x004538AE, `cmdinterp->PlayerTeleported()` @0x004538B3, `set_viewer` @0x004538D5, `LScape::update_viewpoint` @0x004538E2, `CellManager::ChangePosition` @0x00453903 | :92469-92509 | **CONFIRMED**, exactly the contract's order |
| `CommandInterpreter::PlayerTeleported` @0x006B32B0 = `SetAutoRun(0,1)` + tail-jump `SendMovementEvent` | :699036-699041 | **CONFIRMED**. New: `SetAutoRun` @0x006B4850 only acts when `(arg2==0) != (auto_run==0)` (@0x006B4871) — acdream's `CancelAutoRun` early-return matches |
| **Inversion A** — local TELEPORT branch @0x0045415F: `TeleportPlayer(&var_48)` @0x00454168`ConstrainTo(arg2, &var_48, start, max)` @0x0045418A`set_velocity(player, {0,0,0}, 1)` @0x004541B4 → return | :93013-93023 | **CONFIRMED**, including the WIRE-destination anchor |
| FORCE_POSITION branch returns @0x0045409D before every `ConstrainTo` | :92925-92933 | **CONFIRMED** — route 2's no-re-arm rule intact and correctly left force-scoped |
| **Inversion B** — the local hook runs AFTER the placement (from `PlayerPositionUpdated`), opposite to the remote arm's @0x005163EF | :92497 vs 4b-3's citation | **CONFIRMED** |
| `enter_world` is NOT on the portal path | :93797, :93824 | **NEGATIVE CONFIRMED** — but the attribution in the new correction banner is wrong; see R6 |
| **P1 (TransientState not re-seeded)** — retail `CPhysicsObj::SetPositionInternal(CTransition*)` @0x00515330 derives Contact from `collision_info.contact_plane_valid` @0x00515430, WaterContact from `contact_plane_is_water` @0x00515453, OnWalkable from `set_on_walkable(contact_plane.N.z vs floor_z)` @0x00515467-0x0051548E, Sliding from `sliding_normal_valid` @0x005154E1. **No unconditional `Contact\|OnWalkable` seed anywhere.** | :283484-283519 | **THE FINDING IS CORRECT AND IS A FIDELITY GAIN.** `PhysicsObjUpdate.CommitSetPositionContactPrefix` (`src/AcDream.Core/Physics/PhysicsObjUpdate.cs:154-176`) is that exact port, and runs inside the canonical commit (`RuntimeSetPositionState.cs:5038`). The old `SetPositionCore` seed (`PlayerMovementController.cs:1859-1862`) was the divergence; dropping it is right. The `Active` argument also holds — `PlayerMovementController.cs:2069` and `:2449` re-set it every frame. |
| `ConstraintManager::ConstrainTo` @0x00556240 initializes `constraint_pos_offset = distance(anchor, m_position)` | :353528-353537 | **CONFIRMED** — acdream matches; also the basis for R3 |
| `CommandInterpreter::SendMovementEvent` @0x006B4680 is autonomy-gated | :700274-700313 | **CONFIRMED** — see R7 |
## §B — implementation facts verified correct
- **D-T3 duty map (P1) is complete.** All nine `SetPositionCore` duties
land in `CommitCanonicalTeleportFrame`
(`PlayerMovementController.cs:1993-2041`) or the canonical commit, in
`SetPositionCore`'s own order (StopCompletely → input/mouse resets →
UnStick/UnConstrain/re-arm → edge resets → clock reset). Nothing
silently dropped except the TransientState seed, which is correct
(§A).
- **Inversions implemented in the right directions.** The portal arm
consumes the classifier's dormant LocalPlayer-teleport branch
(`RuntimeAuthoritativePositionRouteClassifier.cs:336-356`) unchanged;
`ConstrainPhase.AfterPositionOperation` + `ZeroVelocity: true` +
`SendPositionImmediately: false` + `TeleportHookPhase.AfterPositionOperation`
all flow through. The hook tail runs only from the committed receipt.
- **The force arm is untouched.** The only route-2 edits are one
force-scoping doc sentence (`:133-137`) and a non-`required`
`Portal { get; init; }` on `Pending` that defaults empty. Zero route-2
test expectation changes — §4 item 8's tripwire is clean.
- **The synthetic `priorTeleport = accepted - 1`** in
`ClassifyPortalArrival` is sound: `TeleportAdvanced` reads only the
boolean `PhysicsTimestampGate.IsNewer(prev, accepted)`, the branch's
resulting route does not depend on the previous stamp's magnitude, and
wrap is safe at `accepted == 0`.
- **AD-42 deleted** (row gone, header 49 → 48 rows), **AD-2 amended in
place** with the deferred-place timing, the T8 tolerated-overwrite
note, and the leash-anchor nuance. Plan gap-line correction present.
Register rules satisfied.
- **Release build green.** Focused
`RuntimeAcceptedPositionDriveControllerTests`: 22 passed / 0 failed /
**0 skipped**. No `Skip` attribute remains in any of the four touched
test files, and no Runtime test was weakened — the five new ones are
strong (the happy path pre-arms the leash at a *stale* anchor so a
missing re-arm fails the `ConstraintPos` assertion; the refusal tests
assert positive "nothing moved, transit still active, no packets"
facts).
## §C — the production bug fix: correct, complete, no stale-destination hazard
Verified. `RuntimeWorldTransitState.TryBeginPortalReveal:159-182` sets
`_hasAcceptedDestination = false` and `_acceptedDestination = default`
the instant it claims the generation, and
`TryGetAcceptedTeleportDestination:522-527` returns
`_teleportActive && _hasAcceptedDestination`. So the pre-fix Place-time
re-read was **guaranteed** to fail — the canonical portal arm was 100 %
dead code, refusing with `cause=host-token-unavailable` before ever
reaching Runtime. The diagnosis is right and this was a real production
bug, not a fixture artifact.
The fix is the right lifetime:
- `_pendingDestination` is written in `AimDestination:781-784`, in the
same statement block as `_pendingCell`/`_pendingRotation`, only after
`TryBeginPortal` succeeded (`:741-748`) — so the four Aim-time
snapshots are mutually consistent by construction.
- **Cancellation:** `ResetTransit:801-804` clears all four; every
cancellation path funnels through it.
- **Supersession:** a second accepted destination re-enters
`TryAimAcceptedDestination``AimDestination`
`WorldRevealCoordinator.TryBeginPortal``WithdrawHostForReplacement`
+ a **new** generation, overwriting all four snapshots together. A
superseded destination cannot survive.
- **Staleness at Place:** three independent gates still validate —
`CanPlacePortalDestination(_pendingRevealGeneration, sequence, _pendingCell)`
(`:507-511`), the idempotent
`TryRegisterHostProjection` re-derivation (generation ==
`_snapshot.Generation`, cell == `_snapshot.DestinationCell`,
`!Cancelled`, `!Completed``RuntimeWorldTransitState.cs:189-227`),
and `BeginAcceptedPlacementCore`'s own
`portal.Projection.DestinationCell == acceptedPosition.LandblockId`
against the **latest merged** snapshot
(`RuntimeSetPositionState.cs:1528-1534`).
The re-read was protecting nothing. `WorldRevealCoordinator.BeginHostLifetime`
throws if the Aim-time registration fails, so the Place-time
re-derivation is genuinely idempotent and can never mint a second host
projection in production.
## §D — contract open questions, answered
**(b) leash anchor — keep the resolved anchor as shipped.** Retail's
`constraint_pos` is write-only (never read by `adjust_offset`, confirmed
in the port's own doc at `ConstraintManager.cs:41-44` and against ACE);
the only downstream consumer of `ConstrainTo`'s inputs is
`constraint_pos_offset = distance(anchor, m_position)`, which is the
placement adjustment (centimetres) in retail and exactly 0 in acdream.
Both are orders of magnitude inside the `0.9 * max` band, so no behaviour
in the leash's brake taper can distinguish them. The AD-2 note is the
right disposition; do **not** switch anchors.
**(c) `SendMovementEvent` shape — see R7.** Message family, contact
derivation, count, and ordering are all correct; the missing
`autonomy_level` gate is the one delta.
## §E — errors in the contract itself
1. **§4 item 3 / D-T5's re-attempt reasoning is the proximate cause of
R1.** D-T5 offered "the anim event re-fires while `ready` holds" as
the leading case and demoted the one-shot case to a parenthetical
verify-and-state. It is one-shot. The contract should have read the
sequencer before writing the row and pinned the driver. This is
process rule 1 ("the contract causes the defect") recurring for the
third documented time.
2. **§9's `leash=armed` criterion is unachievable** with any
`IsFullyConstrained`-shaped observable; the contract should have named
`ConstraintManager.IsConstrained`. See R3.
3. **§3.5 overstates the change:** "`AcknowledgePortalMaterialized` fires
from the committed placement receipt instead of rubber-stamping after
a host mutation." As built it still fires from the host Place edge,
merely gated on a committed status. Substantively equivalent on the
commit path; wording should be corrected so a future reader does not
look for a receipt-driven ack that does not exist.
4. **§1's `enter_world` row** carries the same wrong caller attribution
the banner does (R6) — it says "its local-player caller is the initial
login path only (@0x00455095)", which is right about that site but
silently drops the *other* site @0x004550EC and mis-names the
enclosing function in the derived correction.
## What must land before this can pass
1. A re-attempt (or explicit-cancel) mechanism for a non-`Committed`
Place edge, with the driver named and tested — R1.
2. Headless must consume the arm's status and must not acknowledge a
materialization for a placement that did not commit — R2.
3. Fix the probe's `leash` observable (and emit a line per *attempt*,
under the gate's own env var) — R3, R8.
4. Write the App-layer presentation suite (§8 items 8/9/10) closing route
2's B2 gap, plus one headless committed-portal test — R4, R5.
5. Correct the `enter_world` caller-sweep banner — R6.
6. Gate the movement refresh on autonomy, or file the register row — R7.
7. Comment/citation cleanups — R9, R10, R11.