acdream/docs/research/2026-08-04-c4-route-3-retail-review-round2.md
Erik e0f96a55bf 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>
2026-08-05 03:57:37 +02:00

22 KiB
Raw Blame History

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 FIXEDConstraint?.IsConstrained at RuntimeAcceptedPositionDriveController.cs:838
R4 App presentation suite missing; assertions weakened PARTIALLY closed, does not block — §D
R5 headless dual-host parity untested FIXEDHeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake
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 FIXEDAP-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

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. TryAdvancePortalCommitTryExecuteAcceptedPortalArrivalTryPrepareAndSubmitAuthoredPlacementcanonical body commit (CommitCanonical, retail SetPositionSimple @0x005162B0).
  2. ReconcileAndAcknowledgePortalCommitCanonicalTeleportFrame (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 AfterPositionOperationcorrect 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.Asserts 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 OnTeleportStartedResetTransit, 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.RebucketLiveEntitycommitPose 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 discriminatingcovered (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

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.