acdream/docs/research/2026-08-04-c4-route-7-contract.md
Erik 392c1e22c1 fix(physics): bind a parented child to the parent's live incarnation (#319)
A player-parented child never received a canonical cell. Its FullCellId stayed
0 for its whole attached lifetime, so it could not follow the player across a
boundary. Scope was wider than the local player: every REMOTE player's
equipment too.

ROOT CAUSE. EquippedChildRenderController hardcoded ParentInstanceSequence: 0
for a parented CreateObject. Correct for creatures and statics, which really
are sequence 0; wrong for players, whose ObjectInstance is Character.TotalLogins
(ACE Player_Networking.cs:37). The relation filed under (playerGuid, 0) while
the record carried TotalLogins, so both route-7 write sites — D1's attach
re-cell and D2's propagation lookup — keyed on an incarnation that never
matched. TryCommitParent did not validate the sequence, so the attach
succeeded and printed normally. Silent.

A ROUTE 7 REGRESSION (cd3129e9) that un-masked a latent bug: the TickChild call
route 7 deleted was keyed on the child guid alone and was structurally immune
to a wrong parent key.

THE FIX IS TO STOP TREATING PLAYERS DIFFERENTLY, not to special-case them.
Retail's attach path is guid-only end to end — PhysicsDesc::get_parent_id
@0x00558a18 -> CObjectMaint::GetObjectA @0x00558a2d -> set_parent @0x00558a3e,
with SetChildren @0x00509370 hash-walking by guid — and neither set_parent
overload (@0x00515A90, @0x00515B50) nor enter_cell @0x00510ED0 contains any
player test or instance-sequence read. Our player/non-player split was purely
an artifact of keying relations by (guid, incarnation) against a wire message
that carries no parent incarnation. Late-binding to whoever currently holds the
guid is retail's own semantics. Fixed at BOTH producers: OnSpawn and
OnCreateParentAccepted, the second carrying the byte-identical defect and not
named in the contract's scope line.

THE INVARIANT IS EQUALITY, NOT FRESHNESS. The contract rejected both framings I
offered: every one of the 45 FullCellId liveness predicates excludes a
committed child on a NON-cell clause first, so the child inherits only the
parent record's existing staleness, which is already present today with no
symptom. The key fix alone restores child-equals-parent for every parent class.

TWO SITES GATED, inert only because the cell was zero and would have woken
wrongly: the hydration candidate loop (a nonzero-cell child would take the
legacy RebucketLiveEntity -> CommitRebucket, a second canonical writer — route
7's exact defect class) and RestoreShadow (would install a broadphase row for
the weapon, the #184 shape, contradicting route 7's P4). Retail anchor:
update_object's parent != 0 early-out @0x00515D40 — children are never
independently re-placed.

THREE MAJORS WERE FIXED BY DELETION. The first pass added a deferral queue for
an unaddressable parent, carrying a missing child-freshness gate (A2), a
sentinel-0 collision with the generation filters (A3), and unbounded
accumulation (A5). Both reviewers then proved the deferred branch unreachable
for BOTH producers — RegisterEntityCore defers the entire CreateObject one
layer above, reading the same ?? chain, and CreateParentUpdate is produced only
inside AcceptCreateCore, after that gate passes. The machinery was deleted
rather than repaired, and the diff SHRANK to 76 added / 13 removed from 91/24
while gaining the A1 fix. Retail confirmed the deletion does not diverge:
acdream's real port of retail's per-guid replay (QueueBlobForObject) is a
different, untouched layer, and the deleted queue was a third redundant one
downstream of it.

THE GUARD MUST NOT TEAR WHAT IT PROTECTS. The first pass threw
InvalidOperationException AFTER the canonical half had committed, so the one
time it fired it left the child parented with no committed relation and a
staged one blocking Resolve — a torn transaction, the exact outcome the
contract pinned against. Now a pure CanCommitIncarnation precondition checked
BEFORE the commit at both sites, with a logged refusal instead of a throw.
Route 3's N3 principle (do not make a transient fatal on a host that must
survive 30 sessions x 2 hours) reinforces it, but the tearing argument stands
alone.

TEST QUALITY, the recurring lesson in its most refined form. The A1 test
initially passed sabotage FOR THE WRONG REASON: a mismatched ChildPositionSequence
meant TryCommitParent's own gate refused in either ordering, so the three
assertions carrying A1's meaning passed both ways and only an incidental
staging assertion failed. It failed on stranding, not tearing. Corrected, the
sabotage now names line 925 — Assert.Null(snapshot.ParentGuid), with the
parent's guid in it — proving the canonical mutation happened before the catch.
"Fails under sabotage" is necessary, not sufficient; WHICH assertion fails is
the real question.

The dual parent-class matrix (player 0x5… incarnation > 1 vs creature 0x8…
incarnation 0, identical outcomes, sabotage-verified in both directions) is the
structural fix for how this survived a full dual review and two connected
sessions: every prior test and both captured gate logs used sequence-0 parents.

Register: AP-142 clause (f); AP-132 amended to distinguish the two producers;
new row AP-146 for the local player's coarse canonical cell (retail writes it
per tick at SetPositionInternal @0x00515330 — which, per the retail review, ALSO
walks this->children writing each child's objcell_id @0x005153AE-@0x005153D8,
so retail's per-tick child propagation lives in the same function). That
divergence had no row at all, a standing rule-1 violation now corrected.
Follow-up #320 filed for making the player's cell track ordinary movement —
deliberately excluded here: it touches the landblock-preserve contract, the
Rebucketed cadence, route-2/4b-3 classification inputs AP-136/AP-138 spent four
review rounds pinning, and the portal-space frozen-source-cell race.

Two dual review rounds; 6 architecture MAJORs and 2 retail MAJORs closed.
Diagnostic refusals are latched per child guid and the latch clears on
Clear()/RemoveChild, so a recycled guid's next incarnation still logs rather
than being silently suppressed.

Complete Release suite MEASURED at 11,112 passed / 4 skipped / 0 failed
(baseline 11,090 at 52175aa1, +22). Neither known flake fired.

STILL OWED: the connected gate, with the CORRECTED positive criterion — assert
the equipped child's FullCellId EQUALS the parent's after a crossing (a zero is
a failure, not a silence), run with BOTH a player and a creature parent, plus
the new step carrying an armed creature across a landblock unload/reload.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 11:56:31 +02:00

66 KiB
Raw Permalink Blame History

C4 route 7 — pickup / parent / delete: pinned contract (2026-08-04)

Scope: make Runtime the sole writer of a parented child's canonical cell — port retail set_parent's attach-time change_cell half into the Runtime parent commit, add retail's parent-cell-crossing propagation step at the one canonical cell-write funnel, demote App's render-tick child rebucket (EquippedChildRenderController.TickChild) to presentation-only, give the headless host the parent-realize commit it has never had, adopt retail's pickup ordering, and delete the dead ClassifyLeaveWorld classifier entry. Route 7 performs no placement: there is no SetPosition, no park, no service window, no leash on this route.

Pinned at HEAD cff52c44, clean tree, branch claude/acdream-physics-divergence-5aa784. Line numbers in this contract are as-of cff52c44 and WILL go stale; every citation also names the symbol — trust the symbol (process rule 6).

Predecessor documents, binding where they still apply:

  • 2026-08-04-retail-parent-cell-propagation.mdthe settling research. Its §10 contract requirements are BINDING and restated in §0 below. Do not re-derive the retail mechanism; it is read, cited, and offset-verified there.
  • 2026-08-04-retail-child-cell-ownership.md — the earlier child-cell research: set_parent contains no cell write of its own; unset_parent performs zero cell work; leave_world is where a detaching child is scrubbed.
  • 2026-08-04-c4-routes-6-7-scoping.md §7 — the route scoping. Its file:line references predate routes 4b-3/5, the OnPosition collapse, and route 6's closure, and are stale throughout; §10 of this contract lists every claim found false or superseded. Its trap list (T1T8) survives and is resolved item-by-item below.
  • 2026-08-04-c4-route-5-contract.md plus its three dual review rounds — the contract standard, and the recurring defect classes each addressed by name here: an App glue site discarding the Runtime seam's status and advancing presentation on write-nothing outcomes; unrecorded divergences (register rule 1); a pinned obligation left unwired; zero coverage of the presentation layer; negative-only tests.
  • 2026-08-04-c4-route-4b-3-contract.md — its 13 "must REMAIN true" invariants; the ones route 7 can even reach are re-asserted in §3.
  • 2026-08-04-session-handoff-c4-remaining.md — the six process rules apply verbatim. Rule 1 (the contract causes the defect), rule 4 (assert the layer that broke), and rule 5 (a clean session is not a passed gate) are the load-bearing ones for this route.
  • docs/plans/2026-08-02-placement-cutover.md — the campaign plan. Its corrected route-7 gap statement ("the child's canonical cell has two writers … the same defect seen from two sides") is exactly this contract's scope; the plan also pins T8 (the TryCommitParent LeaveWorld omission is retail-REQUIRED).

Sequencing: routes 4a, 4b-1/2/3, the OnPosition collapse, route 5 (36255af0), and route 6's zero-production closure (1b484937) are all in. Route 7 is next; route 3 (portal) remains after it.


0. Facts settled before this contract — BINDING, do not re-derive

From 2026-08-04-retail-parent-cell-propagation.md (all addresses verified against acclient_2013_pseudo_c.txt, struct offsets closed by the acclient.h walk — no PE byte-decode needed):

  1. Retail re-cells children when the parent crosses a cell, recursively, to unbounded depth. CPhysicsObj::SetPositionInternal @0x00515330 branches on this->cell == curr_cell @0x0051536d; the cell-CHANGED branch @0x00515372 calls change_cell @0x00513390, which delegates to leave_cell @0x0051339f / enter_cell @0x005133af. change_cell itself has NO child loop — the recursion lives in the delegates. enter_cell @0x00510ed0 self-recurses over children @0x00510f03 and writes the FULL identity per level: CObjCell::add_object @0x00510ee2, objcell_id @0x00510f1e, part-array cell id @0x00510f2b, cell pointer @0x00510f35, lights @0x00510f3e. leave_cell @0x00510f50 mirrors it @0x00510f84. CORRECTION (post-implementation retail-conformance review, R1 MAJOR): this enumeration silently dropped the guard AROUND all five writes and the recursion itself — enter_cell's entire body is gated on this->part_array != 0 @0x00510ed8 (the propagation research's own §3 called this "Guard, load-bearing"). A child with a null part array receives NONE of the five writes and its whole subtree is skipped. This is the contract defect the review traced R1 to — process rule 1, "the contract causes the defect" — and it is why an implementer following this list alone ships an unconditional write. See D2's AP-142 clause (d) for why the guard has no reproducible analogue at acdream's canonical layer (its HasPartArray field is populated only by the graphical mesh pipeline, never headless) and is therefore recorded, not ported.
  2. The depth-1 loop @0x0051539c0x005153d8 is the SAME-CELL fast path, NOT the propagation. It refreshes only each direct child's objcell_id (child +0x4c @0x005153bd) and part-array id @0x005153cc, deliberately not the cell pointer, and only when the parent did NOT change cell. An implementer who finds this loop first will wrongly conclude "depth-1, id-only" and ship a stranded-child bug. This contract says so explicitly: the propagation is the else @0x00515372, not this loop.
  3. The clincher: update_object @0x00515d10 early-returns on parent != 0 @0x00515d40 — a child never runs its own physics tick, so parent propagation is the ONLY mechanism that maintains a child's cell.
  4. The four binding requirements (research §10): (i) write at attach AND on every parent cell crossing; (ii) the authoritative write belongs on the physics-commit path — Runtime's write must be a PROPAGATION STEP, not a one-shot at set_parent; (iii) propagation is recursive — a depth-1 implementation needs an explicit stated assumption plus a register row; (iv) write the full identity — id-only leaves the #184 class half-closed.
  5. Two adjacent traps: (a) child cross-cell/shadow lists are NOT refreshed per parent tick — SetPositionInternal calls the non-recursive calc_cross_cells @0x0051551b; the recursive recalc_cross_cells @0x00515a30 runs only at attach (set_parent @0x00515b15). Do not rebuild child shadow registrations per crossing. (b) On the removal path (change_cell with a null target) retail leaves children with cell == nullptr but a STALE non-zero objcell_id @0x005133c1 — leave_cell never touches child ids. §4 D3 resolves how acdream's single-field model maps this.

From this contract's own HEAD verification:

  1. acdream DOES use FullCellId != 0 / == 0 as a residency/liveness predicate, pervasively — 45+ sites, including RuntimeInitialCreateResidenceState:583 (residence admission), LiveEntityRuntime isOrdinaryRoot (:915-918) and its two sibling predicates (:3213, :3323), LiveEntityPresentationController:220, RuntimeSetPositionState:2985 (the lost predicate), HeadlessLocalPlayerFrameHost:87, and route 4b-3's cell-less classification input (PreMergeCommittedCellId == 0 → the SetPosition cell-less arm). The retail stale-id asymmetry (item 5b) therefore MUST NOT be reproduced literally — see D3.
  2. Route 5 and route 6 landed after the scoping, so the scoping's "route 6 first" ordering and its campaign-plan correction are already satisfied (1b484937 corrected docs/plans/2026-08-02-placement-cutover.md:97-116).
  3. The canonical cell has exactly ONE funnel. RuntimeEntityRecord.SetFullCell (RuntimeEntityRecord.cs:244-251) has exactly two callers: RuntimeEntityDirectory.SetFullCell (RuntimeEntityDirectory.cs:340-346) and RuntimeEntityRecord.RefreshDerivedState (:230-242), and RefreshDerivedState is itself reached only from the record constructor (:29, no children can exist yet) and RuntimeEntityDirectory.RefreshSnapshot (:231-238). Every producer — CommitRebucket (RuntimeEntityObjectLifetime.cs:1863-1894), RuntimePhysicsState.CommitCanonicalCell (:2138-2160, fed by the ordinary/remote/projectile simulation commits and the remote writeCell binding :958-960), RuntimeSetPositionState's four direct writes (:2743, :3660, :5001, :5222), the withdrawal family (SetFullCell(canonical, 0u, 0u) at :1301, :1464, :1921, :2660), and the wire merge (RefreshSnapshotRefreshDerivedState) — funnels through the directory. This is what makes D2's single-chokepoint design sound rather than a per-caller mapping (the 4b-3 review's "mapping written against one caller's reachable set" defect class).
  4. The per-parent committed-children list already exists in Runtime. ParentAttachmentState.ChildrenAttachedToParent(parentGuid, parentInstanceSequence) (ParentAttachmentState.cs:623-633) returns the exact live CHILDLIST analog (doc comment already cites retail's live CHILDLIST), maintained by CommitProjection (:546-572) / RemoveCommittedChild (:809-838). Its two existing consumers are the lost-family deadline arm/cancel (RuntimeSetPositionState:6024-6036, :6046-6060). It allocates nothing on the read path (returns the stored List<uint> or Array.Empty).
  5. Parented children's snapshots carry no Position. InboundPhysicsStateController.ApplyParent (:1347-1373) sets Position = null (top-level AND PhysicsSpawnData); ApplyAcceptedParent / ApplyAcceptedCreateParent are timestamp-only. So the wire merge's RefreshDerivedState cell stamp (Snapshot.Position is { } position, RuntimeEntityRecord.cs:232) cannot fire for a committed child and cannot fight the propagation. P1 pins this with a test.

1. Site inventory — re-located at cff52c44

Every site verified by reading at HEAD, not inherited from the scoping.

1.1 The Runtime commit family (src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs)

symbol at HEAD (was, in scoping) route-7 relevance
TryApplyPickup :1249-1312 (was :1226-1245) pickup: gate → RefreshSnapshotForgetInitialCreateResidence :1293AdvancePositionAuthority :1294CollisionReports.LeaveWorld :1295SetPosition.Forget :1297SuspendObjectClock :1300SetFullCell(0,0) :1301ParentAttachments.EndChildProjection :1302 → publish Withdrawn. T7's inversion lives at :1295-1302 (leave-world work before the unparent). Dormant-residence deferral :1255-1281.
TryApplyParent :1336-1386 accepts/stages the standalone ParentEvent; dormant-residence deferral :1342-1379; live path Entities.TryApplyParentCommitPositionChannelUpdate. Untouched by this slice.
TryApplyCreateParent :1314-1334 envelope flavor; untouched.
TryCommitParent :1388-1441 (was :1360-1374) the parent-relation commit (retail add_child-success analog). Carries the C0-4(a) cancellation chokepoint :1426-1431 and the F4 deliberate LeaveWorld omission comment :1418-1425 (T8 — do not "fix"). AdvanceParentCommit :1432. D1 does NOT add the re-cell here — see D1 for why it lives on the cell-less commit's successor instead.
CommitAcceptedParentCellless :1443-1474 (was :1377-1408) retail set_parent's leave_world edge: cancellations → CollisionReports.LeaveWorld :1462SuspendObjectClock :1463SetFullCell(0,0) :1464 → publish Withdrawn. D1's extension point: the missing parent->cell != 0change_cell half goes immediately after this edge.
TryApplyPosition's unparent edge EndChildProjection at :1838, after RefreshSnapshot :1830 the Position-unparent (retail HandleReceivedPosition's unset_parent @0x00454129). Same inversion shape as T7 but on route 4's surface — recorded in §9 as a non-goal, NOT touched here. Also carries 4b-3's PreMergeCommittedCellId measurement :1801-1814 — see §11 for the cross-contract interaction.
CommitRebucket :1863-1894 the App rebucket's canonical write; publishes Rebucketed on an actual cell change. After D4, no equipped-child caller remains.
CommitWithdrawal :1896-1929 (was :1845-1860) withdrawal-to-cellless with the C0-4(b) symmetric cancellation; SetFullCell(0,0) :1921. D2's propagation covers its children automatically.
TryAcceptDelete :1973-2034 (was :1939-1952) ParentAttachments.DeleteGeneration runs at :1996-1998, BEFORE the active record retires (RemoveActive :2005), and the delete path performs NO SetFullCell — so D2's chokepoint never fires for a deleted parent's children and D3's explicit delete edge must run before :1996.
ForgetInitialCreateResidence / PreferCancellation :2620-2637 / :2639-2642 (was :2552-2569 / :2571-2574) unchanged by this slice.
AcknowledgeProjectionAndPublish :2255 on (was :2187-2213) publication discipline: cancellation receipt first, then currency re-check, host ack, publish. Unchanged.
CommitChildNoDraw :1931-1950 retail set_parent's NoDraw inheritance — already ported; untouched.

1.2 The classifier (src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs)

symbol at HEAD relevance
RuntimeLeaveWorldCause :31-36 deleted by D6.
RuntimeLeaveWorldRouteRequest :141-145 deleted by D6.
ClassifyLeaveWorld :480-510 (was :475-505) ZERO production callers at HEAD (re-verified: repo-wide grep returns the definition, one test at RuntimeAuthoritativePositionRouteClassifierTests.cs:335-352, and a comment at RuntimeInitialCreateContinuationExecutorTests.cs:1672). Deleted by D6, with the test.
ValidCreateAuthority :512-516 (was :507-512) requires PreviousTeleportSequence == AcceptedTeleportSequence — the #307 predicate shape. T3 verified first-hand: no pickup/parent gate measures a teleport pair (InboundPhysicsStateController.TryApplyPickup :180-186 gates on TryAcceptPositionChannelEvent — retail's POSITION stamp @0x0045224B analog; TryApplyParent :263-271 adds only parent-instance currency; TryCommitParent :308-314 re-checks POSITION_TS currency). Wiring the classifier would force a fabricated, vacuously-equal teleport pair. This is D6's second leg. ValidCreateAuthority itself SURVIVES (the create route uses it); only the leave-world consumer dies.

1.3 App (src/AcDream.App)

symbol at HEAD relevance
EquippedChildRenderController.TickChild Rendering/EquippedChildRenderController.cs:373-413; the rebucket at :406-408 (was :405-408) the render-tick canonical writer: after pose composition succeeds, _liveEntities.RebucketLiveEntity(child.ChildGuid, parentCellId). D4's demotion target — the ONLY RebucketLiveEntity call in the file (re-verified).
EquippedChildRenderController.PrepareAndTryRealize :841-885 the graphical realize protocol: CommitStagedParent (→ TryCommitParent) :856Relations.CommitProjection :857CommitAcceptedParentCellless :869-871WithdrawPriorProjection :875-881TryRealize. D1's attach re-cell rides inside the Runtime commit this already calls — the App protocol does not grow a fourth call.
EquippedChildRenderController.ValidateParentProjection :887-912 retail add_child validation (Setup HoldingLocations via _dats.Get<Setup>) — graphical-only today. D5's headless validation question.
EquippedChildRenderController.ResolveRelations :786-795 drives Relations.Resolve with snapshot-lookup callbacks — the resolution shape D5's headless drive reproduces Runtime-side.
LiveEntityRuntime.RebucketLiveEntity World/LiveEntityRuntime.cs:801-977 (scoping's range still accurate) the full legacy branch: spatial bucket + CommitRebucket :904-907 + object-clock edges :919-946 (whose own comment already states "parented/attached objects take retail update_object's parent early-out and remain suspended") + visibility publication.
LiveEntityRuntime.RebucketLiveEntityPresentationOnly :993-1065 (was :993-1050) the C3c presentation-only shape: spatial bucket + visibility, deliberately no CommitRebucket / clock work, guarded by BeginPresentationOnlySpatialMutation. Private; sole caller TryApplyInitialCreateCompletionPresentation :1107. D4 adds the equipped-child entry point beside it.
LiveEntityRuntime wrappers TryApplyPickup :2312-2316; CommitStagedParent :2330-2336; CommitAcceptedParentCellless :2338-2363 (was :2280-2312) the cell-less wrapper's doc (:2338-2343) still says "Commits retail set_parent's cell-less edge" — accurate only for parent->cell == 0; D9 corrects it with D1.
LiveEntityHydrationController.OnPickup World/LiveEntityHydrationController.cs:460-474 (was :455-474) TryApplyPickup then _relationships.OnChildBecameUnparented — App-level order unchanged by D7 (D7 reorders INSIDE the Runtime method).
LiveEntityDeletionController World/LiveEntityDeletionController.cs purely logical (re-verified: no placement/cell API). Untouched.

1.4 Headless (src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs)

symbol at HEAD relevance
OnParentUpdated :312-316 (was :313-317) calls ONLY Entities.TryApplyParent — stages the relation forever. Neither TryCommitParent nor CommitAcceptedParentCellless has any headless caller (re-verified: the only production callers are EquippedChildRenderController.cs:856/:869 and the LiveEntityRuntime wrappers). D5's insertion point.
OnPickedUp / OnDeleted :176-180 / :155-174 thin pass-throughs; correct as-is.

1.5 Confirmed-clean (scoping §7.3, re-verified at HEAD)

All six cancellation choke points live and symmetric (:1141, :1293-1299, :1426-1431, :1456-1461, :1913-1919, :2007-2017); ordering correct (AcknowledgeProjectionAndPublish publishes the cancellation first); receipts host-visible (RuntimeSetPositionState.PublishCancellationPublishPlacement, consumed by both graphical sinks and HeadlessRuntimePlacementProjectionSink); pickup/parent during a pending residence defer as dormant continuations; grep for SnapToCell|CommitRebucket|SuspendObjectClock|SetFullCell across src/AcDream.App + src/AcDream.Headless still returns no pickup/parent/delete site outside the inventoried ones.


2. Retail ground truth — verified for this contract; verify again yourself

claim anchor status
Pickup = unset_parent + leave_world, gated ONLY on the POSITION stamp; no placement, no rejection path that skips them once the stamp accepts SmartBox::DoPickupEvent @0x00452240: gate @0x0045224B-0x00452274, stamp write @0x00452278, unset_parent @0x0045227F, leave_world @0x00452286 CORRECTED (retail-conformance review): the gate span itself is UNVERIFIABLE from this source — Binary Ninja lowered both DoPickupEvent's and DoParentEvent's gate comparisons to a literal always-false expression (if (-((eax_4 - eax_4)) != 0)), losing the x87/flag-based wrapped-sequence compare. Only the gate's SHAPE (a wrap-aware sequence compare against update_times[0]) and the ORDER of the writes after it are legible — the write order is what D7 relies on and remains ✓. Was previously marked "✓ (scoping §7.2, re-read)", which overstated what the source supports.
Parent = set_parent + SetPlacementFrame; SetParentedState(1) for a non-player parent gaining its first child SmartBox::DoParentEvent @0x00452290: gate @0x00452296-0x004522C5, @0x004522F4, set_parent @0x00452305, SetPlacementFrame @0x00452313 CORRECTED, same basis as the row above: the gate span is unverifiable from this source (same BN lowering artifact); the post-gate write order is legible and unaffected. D6's argument does not depend on this citation either — it rests on acdream already enforcing the POSITION_TS gate in InboundPhysicsStateController (verified directly), not on retail's gate expression being readable.
set_parent order: add_child success → unset_parent @0x00515ABA → one leave_world @0x00515AC1 → parent = @0x00515AC6 → if (parent->cell != 0) @0x00515AD1 → change_cell @0x00515AD6 → UpdateChild @0x00515B0E → recalc_cross_cells @0x00515B15 → NoDraw inheritance @0x00515B26-38 CPhysicsObj::set_parent @0x00515A90 (4-arg overload @0x00515B50 same shape)
unset_parent performs ZERO cell work: remove_child → NoDraw restore → parent = nullupdate_timeclear_transient_states @0x00513470 (@0x00513484/@0x005134AC/@0x005134BF/@0x005134CE) ✓ (child-cell-ownership doc §4)
leave_world scrubs the detaching object: remove_shadows_from_cells @0x005155DD, recursive leave_cell(this, 0) @0x005155E6, zeroes only ITS OWN objcell_id @0x005155F4 CPhysicsObj::leave_world @0x005155A0
The propagation mechanism and its cadence — §0 items 13 @0x00515330 / @0x00513390 / @0x00510ed0 / @0x00510f50 / @0x00515d40 ✓ (settling research; binding)
Delete order: exit_world @0x0050846B + leave_world @0x00508472 run BEFORE unparent_children @0x005084B9 — children's cells are nulled by the recursion while still attached, then unparented with no cell restore CObjectMaint::DeleteObject @0x00508460 (same pattern in DestroyObjects @0x00508C30)
Cross-cell/shadow: recalc_cross_cells @0x00515A30 recurses children @0x00515A79 but runs only at attach; the per-move tail calls the non-recursive forms only @0x0051551B/@0x0051553E-4C settling research §8 ✓ (binding trap 5a)
Route 7 never reaches HandleReceivedPosition @0x00453FD0 — DoPickupEvent/DoParentEvent are separate wire handlers; the single remote ConstrainTo arm @0x00454272 is unreachable from this route scoping T1, re-affirmed ✓ — the basis of D8

3. What must REMAIN true (process rule 1 — for every path, including every refusal)

  1. A committed child's canonical FullCellId equals its parent's at every stable observation point — after attach (parent celled), after every parent cell crossing (any writer: simulation commit, rebucket, canonical placement, wire merge), after parent teleport, in BOTH hosts. This is the route's headline invariant and the headless gate's assertion (it fails today).
  2. The child never becomes a self-simulating object. Its ObjectClock stays suspended, it is never a spatial root, it joins no physics workset, and the propagation path never changes any of that (retail update_object's parent != 0 early-out @0x00515D40; the existing comment at LiveEntityRuntime.cs:919-922 already states this rule for the App side).
  3. No placement machinery engages on this route. No RuntimeSetPositionState operation, no park, no DeferredCell, no service-window pre-flight, no ledger entry — the child cell write is retail change_cell: a direct identity write, not a placement (T6). The ParkCollisionResidents overlap throw stays unreachable and RemotePlacementDrivePendingCount is unaffected by any number of attach/crossing/withdraw events (4b-3 invariants 9/10 extended).
  4. ConstrainTo is NEVER armed by route 7 — not at attach, not at pickup, not at delete, not on any child, regardless of what routes 2/4a/4b/5 established for their arms (T1; D8's partition).
  5. The six cancellation choke points and their ordering are unchanged: exactly-once ForgetInitialCreateResidenceSetPosition.ForgetPreferCancellation, receipt published before the entity delta.
  6. TryCommitParent keeps exactly zero CollisionReports.LeaveWorld calls (T8; the F4 comment at :1418-1425 and the campaign plan both pin it — retail set_parent has ONE leave_world @0x00515AC1, and it is the cell-less commit's edge in acdream's staged protocol).
  7. Presentation still advances, and is asserted (process rule 4 / #312's layer): the equipped child renders in the hand, follows the parent across cell boundaries with no frame where it is bucket-stranded, disappears cleanly on unwield/pickup, and its collision leaves the world with it. A child must never be invisible-but-solid (#184) or solid-but-invisible.
  8. No per-crossing child shadow/cross-cell rebuild (§0 trap 5a). The child's broadphase state is established at attach/unparent edges only.
  9. The dormant-residence deferrals are untouched: pickup/parent arriving during a pending initial residence still enqueue RuntimeInitialCreateContinuationKind.Pickup/Parent continuations and replay through the executor (RuntimeInitialCreateContinuationExecutor's parent replay :2110-2126, which already routes through RuntimeEntityObjectLifetime.TryCommitParent).
  10. Route 1/2/4/5 classification inputs and dispositions are byte-identical. Route 7 deletes ClassifyLeaveWorld (zero production callers) and changes NOTHING else in the classifier — ClassifyCreate's Parented/PickedUp residence handling, ValidCreateAuthority's create-route use, and every accepted-position branch stay untouched. Zero expectation changes in surviving classifier tests is the tripwire.
  11. AP-135's writes, AP-131, #276, and #316 are untouched (§9).
  12. The lost-family deadline enumeration keeps working: ArmLostFamilyDeadlines/CancelLostFamilyDeadlines read ChildrenAttachedToParent — D2/D3 change nothing about relation lifetime, only cell values.
  13. Ledger convergence: teardown, session reset, and generation change with committed children present (attached, mid-crossing, mid-unparent) converge the combined ownership ledger to zero — the J-series suites' shape, driven through the new edges.

4. Design decisions — pinned, not open for redesign

D1 — the attach half: Runtime completes retail set_parent, on the cell-less commit

Retail's attach sequence (§2 row 3) ends with if (parent->cell != 0) change_cell(this, parent->cell). acdream's realize protocol today ends at the leave_world edge (CommitAcceptedParentCellless) and lets a render tick supply the re-cell. Pinned:

  • CommitAcceptedParentCellless (or a successor commit it becomes part of) gains retail's second half: after the cell-less edge's existing writes, if the PARENT's canonical record is active and parent.FullCellId != 0, write the child's full canonical cell identity to the parent's exact values (FullCellId, CanonicalLandblockId) — through the same D2 write path, so attach and crossing are one mechanism, not two. If the parent is cell-less (retail parent->cell == 0 @0x00515AD1), the child stays cell-less — exactly today's behavior, now by the retail-cited gate instead of by omission.
  • Both halves are ONE synchronous Runtime transaction. No caller may observe the child cell-less between the edge and the re-cell within the same call; no deferred continuation may interleave. The method needs the parent's identity to do this — the natural source is the committed relation (ParentAttachmentState.TryGetProjection/_lastAcceptedByChild via a lookup, or a parent parameter threaded from the caller, both of which the realize protocol and the executor's parent replay already hold); implementer's choice, pinned constraint: the parent must be resolved by (guid, incarnation) currency, never by guid alone.
  • Why not inside TryCommitParent? Retail's cell write follows the leave_world (@0x00515AC1 precedes @0x00515AD6). acdream's protocol splits set_parent across TryCommitParent (relation commit) then CommitAcceptedParentCellless (leave-world edge); the re-cell belongs after the second, preserving retail's order. Putting it in TryCommitParent would re-cell BEFORE the leave-world edge zeroes it — a self-defeating order. The executor's deferred parent replay and the graphical realize both already call the pair in this order; D5's headless drive calls the same pair.
  • UpdateChild (frame composition) and NoDraw inheritance remain where they are (App pose composition; CommitChildNoDraw) — unchanged.
  • recalc_cross_cells @0x00515B15: acdream's analog at attach is the EXISTING behavior (the child's collision reports were force-ended by the cell-less edge; no child broadphase registration exists to rebuild). Pinned: no new cross-cell/shadow machinery is built at attach, and P4 requires the implementer to state the child's actual broadphase state at each edge with the retail anchor.

D2 — the sustaining half: propagation at the one canonical-cell funnel

The load-bearing decision. Retail's trigger is "every mechanism that changes the parent's cell" — in retail that is one function (change_cell); in acdream the analog is the one funnel every canonical cell write already passes through (§0 item 8). Pinned:

  • The propagation hook lives at the directory funnel — inside RuntimeEntityDirectory.SetFullCell and the RefreshSnapshotRefreshDerivedState derived write (either by routing the latter through the former or by hooking both; implementer's structural choice, pinned outcome: no canonical cell write can bypass the hook). Per-committer hooks (≥8 sites) are REJECTED — that is the "mapping written against one caller's reachable set" defect class, and one missed site is a stranded child.
  • The step: when a record's FullCellId changes and ParentAttachments.ChildrenAttachedToParent(record.ServerGuid, record.Incarnation) is non-empty, write each active committed child's canonical cell to the parent's new exact values, recursively (a child's own committed children follow — retail enter_cell/leave_cell self-recursion, §0 item 1). Depth-1-only is NOT acceptable without an explicit stated assumption plus a register row (§0 item 4.iii) — and since recursion here is a dictionary probe per level, ship the recursion.
  • Termination and idempotence: skip a child whose FullCellId already equals the target value. This terminates any wire-induced relation cycle (self-parenting is already rejected at EquippedChildRenderController.ValidateParentProjection:890-891, but A→B→A via wire must still terminate), avoids spurious SpatialAuthorityVersion churn, and subsumes retail's same-cell depth-1 id refresh (§0 item 2): with one field playing both retail roles, a same-value restamp is unobservable, so the same-cell fast path needs no separate mechanism. This equivalence is a stated assumption of the single-field model and rides in D9's register row.
  • What the step writes: the child's canonical FullCellId + CanonicalLandblockId — acdream's full canonical identity (§0 item 6: the field IS the residency predicate). What it must NOT do: no clock changes, no workset/spatial-root changes, no shadow work, no placement operations, no CollisionReports calls, no App callbacks. Field writes plus version bumps only — safe to run re-entrantly inside a RuntimeSetPositionState/RuntimePhysicsState transaction that is mid-commit on the parent (P5).
  • Publication: per-child lifetime deltas are NOT published from the propagation step, matching the physics-commit precedent (RuntimePhysicsState.CommitCanonicalCell publishes no lifetime delta; it fires CellCommitted, which is parent-scoped and unchanged). The attach re-cell (D1) rides inside a commit that already publishes; the crossing propagation is silent. Uniformity note: today's TickChild path DID publish Rebucketed deltas for children via CommitRebucket (:1889-1893); D4 removes those. P8 requires enumerating Rebucketed consumers and confirming none needs a per-child delta — if one does, flip this default and publish uniformly from both D1 and D2, and say so in the commit.
  • Allocation: 0 B on the propagation path (the children list is the stored list; recursion uses the call stack or a pre-sized scratch — Slice I discipline).
  • Do not propagate to _stagedByChild/_recoveryByChild/unresolved relations — retail's CHILDLIST holds committed children only, and ChildrenAttachedToParent's own doc already pins this ("must not capture staged, unresolved, or future-generation relations").

D3 — the withdrawal and delete edges (resolves §0 trap 5b under the single-field model)

Retail's removal behavior: leave_cell recursion nulls each child's cell pointer but leaves a stale non-zero objcell_id; the functional state is "not resident anywhere." acdream has ONE field, and that field is the residency/liveness predicate at 45+ sites (§0 item 6). Pinned:

  • Withdrawal propagates zero. A parent's SetFullCell(0,0) (pickup :1301, CommitWithdrawal :1921, cell-less parent commit :1464, residence re-begin :2660) flows through D2's chokepoint like any other value: committed children (and their subtrees) go cell-less. This is the functional mapping of retail's recursive leave_cell — the retail stale-id residue is NOT reproduced, because reproducing it would leave a child "resident" per every acdream predicate while retail's own gating field (cell == nullptr) says it is not. The id/pointer collapse and this deliberate non-reproduction are recorded in D9's register row.
  • Delete gets an explicit edge. TryAcceptDelete performs no SetFullCell, and ParentAttachments.DeleteGeneration (:1996) removes the relations before the record retires — so the chokepoint alone leaves a deleted parent's children stranded at a stale non-zero cell, which under acdream's predicates means "still resident" (the #184 shape, until each child's own DeleteObject arrives). Pinned: before DeleteGeneration runs, the delete path applies the children's leave-world edge — for each active committed child of the exact deleted incarnation (recursively), cell-less via the same D2 write path. Retail order anchor: DeleteObject's leave_world @0x00508472 runs before unparent_children @0x005084B9, i.e. children are still attached when the recursion nulls their cells. The children's RELATIONS are then torn down by the existing DeleteGeneration exactly as today; the children's own records stay alive awaiting their own wire terminal (retail: unparent_children does not destroy children either).
  • EndGeneration (ParentAttachmentState:665-694, the replacement- generation path) — same stranding shape, same fix, same edge, applied at its Runtime call site (RuntimeEntityObjectLifetime:1000).

D4 — the App demotion: TickChild becomes presentation-only (resolves T5)

  • EquippedChildRenderController.TickChild:406-408 stops calling the public RebucketLiveEntity and calls a new internal equipped-child presentation rebucket on LiveEntityRuntime — the RebucketLiveEntityPresentationOnly shape (:993-1065: spatial bucket move, visibility resolution, presentation refresh, visibility-change publication, BeginPresentationOnlySpatialMutation guard), with deliberately no CommitRebucket, no clock edges — because after D1/D2, Runtime already owns the canonical commit, which is exactly the C3c precondition that method's doc demands for presentation-only use. (The C3c-R1 R2 warning at :824-832 — "post-residence moves take the full legacy branch" — does not apply: it protects entities whose ONLY cell authority would otherwise be the graphical rebucket; an equipped child's authority is now the D1/D2 Runtime write.)
  • The entry point is child-scoped (assert the record has a committed parent relation, or is called only from the equipped-child controller) so it can never become a general bypass of the legacy branch.
  • T5's regression risk is the acceptance test, not a reason to keep the old writer: route 4a's R1 showed that dropping the bucket move leaves an entity body-correct but draw-bucket-stale (invisible-but-solid). The demoted call MUST still move the graphical bucket every time the parent's ParentCellId changes — TickChild's existing cadence (per recomposition, with ParentPresentationMatches/CaptureParentPresentation change detection on LastParentCellId, :426-446) already provides the trigger; only the canonical half is removed. The connected gate's carry-across-landblock step plus the dual-layer tests (§6) enforce it.
  • All other TickChild effects (pose composition, ParentCellId mirror, draw-visibility inheritance, PublishChildPose, ProjectionPoseReady) are untouched. WorldEntity.ParentCellId remains presentation (AP-133's split is not re-litigated).

D5 — the headless parent-realize drive

RuntimeLiveEntitySessionController.OnParentUpdated (:312-316) grows the realize that headless never had: after TryApplyParent accepts/stages, resolve the staged relation (the ParentAttachmentState.Resolve + TryGetStagedProjection protocol ResolveRelations demonstrates — snapshot-known + instance-currency callbacks, all Runtime-readable) and run the SAME commit pair the graphical protocol runs: TryCommitParentCommitAcceptedParentCellless-with-D1. Also drive the deferred/recovery retry the graphical controller performs on parent arrival (children waiting for a parent that appears later — OnSpawned's projection path), to the extent the direct host receives those events; state what is deliberately not driven (pose composition, which is presentation and does not exist headless).

The validation gap, pinned rather than discovered later: retail's add_child validates the holding location against the parent's Setup (CSetup::GetHoldingLocation @0x0050F896); the graphical host ports this via ValidateParentProjection's DAT read. The headless host reads prepared collision content, which does not expose Setup.HoldingLocations. Pinned: the headless drive commits on gate acceptance + relation resolution alone, skipping the holding-location validation, recorded as a register row in the same commit (a server-sent invalid location would attach headless where retail/graphical reject — unreachable against a well-behaved ACE, but a divergence and it gets its row; precedent: the content-less host's documented reduced-fidelity registration at RuntimeLiveEntitySessionController:108-117). If the reviewer finds HoldingLocations cheaply exposable through existing prepared content, that retires the row — but do NOT extend the bake format for it in this slice (stop-and-report if that seems required).

D6 — ClassifyLeaveWorld is DELETED (resolves T2, informed by T3)

Delete ClassifyLeaveWorld (:480-510), RuntimeLeaveWorldRouteRequest (:141-145), RuntimeLeaveWorldCause (:31-36), and the one pinning test (RuntimeAuthoritativePositionRouteClassifierTests:335-352). Rationale, recorded in the commit:

  • Retail has no classification here. DoPickupEvent @0x00452240 and DoParentEvent @0x00452290 are separate wire handlers dispatching directly; they never reach HandleReceivedPosition. Method-per-cause in RuntimeEntityObjectLifetime IS the retail shape — the scoping's worry ("the cause discriminator is implicit in which method the caller picked") describes retail's own dispatch, not a defect.
  • The only gate retail has is the POSITION stamp, and acdream already enforces exactly that, in Runtime, at InboundPhysicsStateController.TryApplyPickup/TryApplyParent/TryCommitParent (§1.2). Wiring the classifier would ADD a second gate (ValidCreateAuthority's teleport-pair equality) that no pickup/parent path can honestly populate (T3, verified) — a vacuous-or-wrong predicate with the #307 defect shape, plus T2's rejected-classification silent-pickup-drop hazard, for zero behavioral gain.
  • This closes the scoping's "wire it or delete it" demand in the direction the evidence points; the scoping's lean ("wire it") predates the T3 verification and is overridden with cause (§10).

D7 — pickup ordering adopts retail's (resolves T7)

TryApplyPickup reorders to retail's unset_parent-then-leave_world: ParentAttachments.EndChildProjection moves ahead of CollisionReports.LeaveWorldSetPosition.ForgetSuspendObjectClockSetFullCell(0,0) (anchors @0x0045227F before @0x00452286). The cancellation sequence, AdvancePositionAuthority, and the publication discipline are unchanged. Verified inert against the new machinery: the picked-up entity's own D2 propagation consults ITS children, not its relation to its parent, so the reorder cannot change propagation; no in-between callback exists (AcknowledgeProjectionAndPublish runs after both). This retires the recorded inversion instead of carrying the "not proven inert" caveat forward. The sibling inversion on the Position-unparent edge (TryApplyPosition:1830/:1838) is route 4's surface and is NOT touched — recorded in §9.

D8 — the inverse-leash partition, and the guards that do NOT come along

The route-7 column of the campaign's constraint-arm partition — stated so an implementer arriving from 4b-2/4b-3/5 ("arm on nonzero return, on every placement outcome") cannot carry the rule across:

event retail path ConstrainTo? placement? distance/snap guards?
pickup DoPickupEvent — never reaches HandleReceivedPosition never none none
parent (attach) DoParentEvent — same never none — change_cell is an identity write none
parent cell crossing (propagation) SetPositionInternal child handling — the PARENT's own route arms whatever ITS route arms; the child arms nothing never (for the child) none none
delete DeleteObject never none none

Explicitly NOT imported (T4/T6): AP-87's 4 m BodySnapThreshold, the 96 m MaxPhysicsDistance, MoveOrTeleport's near/far split, 4b-1's service-window machinery, and any CanAttemptDestination pre-flight — pickup/parent/delete have no distance concept and no deferrable Core condition (retail's change_cell runs no sweep and no AdjustPosition). If an implementer finds a reason a child cell write CAN defer, that is a new finding: stop and report.

D9 — register and comment bookkeeping, in the implementation commit

  • ONE new AP row — the parented-child cell model (three clauses, all intentional-architecture): (a) acdream collapses retail's cell-pointer/objcell_id pair into one canonical FullCellId that is also the residency predicate; consequently (b) the removal path propagates ZERO to children where retail leaves a stale non-zero objcell_id under a null pointer (@0x005133C1 / leave_cell's absent id write — deliberate non-reproduction, D3), and (c) retail's same-cell depth-1 per-tick id refresh (@0x005153BD) is subsumed by the value-idempotent chokepoint (D2) rather than ported as a tick loop. Anchors: @0x00513390, @0x00510ed0, @0x00510f50, @0x0051539c-@0x005153d8, @0x00515d40.
  • ONE new AP row — headless holding-location validation skip (D5), if the reviewer confirms no cheap prepared-content read exists.
  • AP-136's writer list shrinks: "the equipped-child renderer EquippedChildRenderController.TickChild" dies as a canonical rebucket writer (register line ~287); the surviving non-Position rebucket writer is the projection materializer alone. Update the row and the two doc comments that carry the same claim: RuntimeSetPositionState.cs:4543 and RuntimeRemotePlacementDriveController.cs:1617.
  • Comment corrections (process rule 6, each verified against the code beside it): LiveEntityRuntime.CommitAcceptedParentCellless's doc (:2338-2343) — "commits retail set_parent's cell-less edge" gains the D1 second half; the same class's RebucketLiveEntityPresentationOnly doc ("called ONLY from TryApplyInitialCreateCompletionPresentation") updates for the D4 entry point; TickChild's surroundings; the ParentAttachments.EndChildProjection doc ("after Pickup or a world Position") if D7's reorder makes its phrasing stale; grep TickChild|CommitAcceptedParentCellless|RebucketLiveEntity across src/ + docs/architecture/ and re-point every survivor.
  • No row deletion. AP-124, AP-131, AP-132 (queued-parent incarnation gating), AP-133, AP-135 all survive untouched.
  • ISSUES.md: none closed by this slice unless the implementer finds the headless child-cell defect has a filed number (none found at HEAD — it is recorded only in the campaign plan's gap list; update that list's wording when this lands).

5. Proof obligations (must prove, not assume; stated in the implementation commit)

  • P1 — no merge fights the propagation. For a committed child, every snapshot-mutation path (ObjDesc, motion, state, PVP bitfield, parent re-commit) leaves Snapshot.Position null (§0 item 10), so RefreshDerivedState never stamps a child cell from its own snapshot. One test drives each mutation family against an attached child and asserts the canonical cell still tracks the parent.
  • P2 — the child stays parent-suspended. After attach, after ten crossings, and after a parent teleport: child ObjectClock suspended, not a spatial root, in no workset, no RemoteMotion, body (if any) inactive. (Invariant 2; retail @0x00515D40.)
  • P3 — no placement-ledger engagement. Attach/crossing/withdraw/delete sequences leave RemotePlacementDrivePendingCount, SetPosition operation counts, and park counts at their prior values.
  • P4 — the child broadphase story, stated. What is a child's shadow/broadphase registration at attach, across crossings, at unparent-by-Position, at pickup, at parent delete? The implementer writes the answer down with the retail anchors (leave_world's remove_shadows_from_cells @0x005155DD; recalc_cross_cells at attach only; §0 trap 5a) and confirms the propagation path performs zero shadow work. If a gap is found (e.g. a child shadow that should exist and does not), it is FILED, not silently fixed in this slice.
  • P5 — re-entrancy safety of the chokepoint. The propagation runs inside whatever transaction wrote the parent's cell (RuntimeSetPositionState placement commit, RuntimePhysicsState simulation commit, CommitRebucket, the wire merge). Because it is field-writes-only (D2), it cannot re-enter those owners. Prove with a focused test per writer family plus the existing reset/reentrancy suites green.
  • P6 — zero allocation on the propagation path (the Slice I discipline): a warmed crossing with N children allocates 0 B.
  • P7 — delete-edge ordering. The children's leave-world edge reads ChildrenAttachedToParent BEFORE DeleteGeneration removes the relations; a test deletes a parent with an attached (and a grand-attached) child and asserts both went cell-less.
  • P8 — publication consumers. Enumerate RuntimeEntityChange.Rebucketed consumers; confirm none requires the per-child deltas TickChild's CommitRebucket used to produce, or flip D2's publication default and say so. (This is route 5's A1 lesson applied prospectively: the App/host layer must be shown to tolerate the Runtime seam's chosen silence.)

6. Test plan

Rules (route 5 §7's, verbatim where they apply): assert the layer that historically broke — presentation and canonical cell, not only InWorld/clock; assert positive facts, not only negatives; every new test must fail against a broken implementation (no source-text pins). The dual-HOST discipline is this route's analog of route 5's dual-kind theories: every Runtime-level scenario runs against the Runtime owners directly (headless-shaped) AND through the graphical wrappers, asserting the same canonical outcome — that is what makes the headless gap a failing test rather than a host-specific accident.

Focused Runtime tests (tests/AcDream.Runtime.Tests):

  1. Attach, parent celled (D1): commit pair on a child whose parent has FullCellId = A → child ends at A (positive), Withdrawn-then-re-celled within one call (no observable cell-less escape), collision reports force-ended, clock suspended, POSITION_TS consumed. Companion: parent cell-less → child stays cell-less (the @0x00515AD1 gate), and a LATER parent cell commit re-cells the child through D2 (the deferred-attach catch-up retail gets for free from propagation).
  2. Crossing propagation per writer family (D2): parent cell changed via (a) CommitRebucket, (b) RuntimePhysicsState.CommitCanonicalCell (simulation commit), (c) a canonical placement commit (RuntimeSetPositionState), (d) the wire merge (RefreshSnapshot with a Position) → child follows in every case; grandchild follows (recursion); a cycle (A→B committed both ways by hostile wire) terminates.
  3. Same-cell idempotence: a parent commit to its CURRENT cell leaves child SpatialAuthorityVersion unchanged (the D2 short-circuit, positive form: the child was already correct).
  4. Withdrawal edges (D3): pickup of the parent, CommitWithdrawal of the parent, and residence re-begin each zero the child (and grandchild); delete of the parent zeroes children BEFORE relations vanish (P7); EndGeneration same.
  5. Pickup of the child itself (D7): relation removed before the leave-world writes (order pinned via the relation table's state at the cell write — e.g. a propagation-visible probe or the committed-children list emptiness at SetFullCell(0,0) time), cell zeroed, clock suspended, Withdrawn published with the cancellation receipt first — and the entity's own children (if any) went cell-less too.
  6. Never-arm partition (D8): after attach + five crossings + pickup + delete, no constraint/PositionManager state exists for parent or child beyond what the parent's OWN route had already armed; arm counts unchanged by every route-7 event.
  7. No-placement invariant (P3) and ledger convergence (invariant 13): teardown/reset/generation-change with children attached, mid-crossing, and mid-unparent.
  8. Headless parent-realize (D5): through RuntimeLiveEntitySessionController.OnParentUpdated with a live directory: staged → committed → celled at the parent's cell — the test named by the route-6 scoping as failing today (child canonical FullCellId == parent's at a stable checkpoint), now in-tree and green; plus the deferred flavor (parent arrives after the relation).
  9. Classifier deletion (D6): surviving classifier tests byte-identical (zero expectation changes — the §3 item 10 tripwire).

App-layer tests (tests/AcDream.App.Tests):

  1. The demotion keeps presentation whole (T5/#184's layer): drive the realize + a parent cell change through the graphical stack; assert the child's render entity moved buckets (spatial index / visibility state), ParentCellId mirrors the parent, AND the canonical cell was written by Runtime (not by the presentation path — assert CommitRebucket was not the writer, e.g. via the presentation-only guard). Sabotage check (manual, TWO runs — corrected at the architecture review, A1): break D2's propagation and confirm THIS test fails on the CANONICAL half while presentation still moves; separately, stub out the presentation rebucket call (RebucketEquippedChildPresentation) and confirm THIS SAME test fails on the PRESENTATION half while the canonical cell is still correct. The first implementation round ran only the first of these two and shipped an assertion (child.WorldEntity.ParentCellId) that TickChild writes unconditionally before the demoted call runs — satisfied whether or not the demoted call executes at all — so the presentation half had zero effective coverage despite the contract asking for it. If EITHER sabotage run leaves the test green, the test is asserting the wrong layer; fix the test. Assert against the actual spatial bucket (e.g. a landblock membership query), not a mirror field TickChild writes elsewhere.
  2. Unwield/pickup teardown: after pickup, the child's projection is gone, no bucket residue, no shadow residue (the invisible-but-solid regression assert, stated positively: the cell is 0, the projection withdrew, the relation is gone).
  3. P8's publication check as a test where feasible (a consumer-facing assertion that the graphical host converges without per-child Rebucketed deltas).

7. Gates

SUPERSEDED 2026-08-05 (#319). This section's gate criterion ("A session counts as a pass ONLY if the probe shows the propagation executed") is UNFALSIFIABLE in the presence of #319's defect — a zero-cell player child emits NO [child-cell] line at all, which this criterion reads as "clean" rather than "broken." Two captured gate sessions passed this exact criterion while carrying the defect. The corrected criterion (a positive equality assertion — the equipped child's FullCellId equals the parent's after a crossing — instantiated for BOTH parent classes) lives in 2026-08-05-c4-closeout-handoff.md and is run at 2026-08-05-issue-319-contract.md §7. Do not re-run this section's recipe as written; use the corrected one.

  • Focused: the §6 suites, green.
  • Complete Release suite: $env:ACDREAM_PAK_PATH = "$env:USERPROFILE\Documents\Asheron's Call\acdream.pak", dotnet test AcDream.slnx -c Release -m:1. Baseline 11,063 passed / 4 skipped / 0 failed at cff52c44. The count will move (one classifier test deleted, new suites added) — measure and record the new figure; do not inherit the baseline. Two known flakes, never chase and never conflate: #302 (PortalProjectionTests.ClipToRegion_FrameOwnedStore_…, GC-allocation assertion, App.Tests) and #308 (NakEmissionTests.LossSoak_…, wall-clock deadline, Core.Net.Tests, full-suite load only). If either appears, re-run and say which.
  • Connected two-client gate (user-run) — this route IS user-visible. Probe: ACDREAM_PROBE_CHILD_CELL=1, PhysicsDiagnostics-owned, marked TEMPORARY with the existing probe family; one [child-cell] line per Runtime child-cell write with parent guid, child guid, old→new cell, and cause (attach / propagate / withdraw / delete / headless-attach). A session counts as a pass ONLY if the probe shows the propagation executed (process rule 5; 4b-3's gate precedent — a clean-looking session with zero cause=propagate lines during step 2 is a not-run). Recipe (scoping §7.7, carried):
    1. Equip/unequip cycle — weapon then shield, five times, observer watching: in the hand, at the hand, oriented with the hand, clean disappearance on unwield.
    2. Carry across ≥2 landblock boundaries and back, both directions of observation; expect cause=propagate lines at each crossing, child cell always equal to the player's. Include one indoor/dungeon traversal (EnvCell-to-EnvCell crossings are the high-frequency case).
    3. Pickup: drop the weapon, pick it back up — leaves the ground, no ghost, no invisible collider at the drop site.
    4. Loot an equipped item from a kill (the delete edge under load).
    5. Reconnect with equipment — re-attaches.
    6. Portal recall while equipped — equipment present and following after arrival. Regressions to watch: weapon drawn at the world origin or its last ground position; invisible while equipped; left behind at a landblock boundary (the demotion's specific risk); invisible-but-solid at a former position (#184); child culled while the parent is visible or vice versa. Graceful close per the standing ACE session rule.
  • Headless gate: the §6 test 8 assertion (child canonical FullCellId equals the parent's at a stable checkpoint) — the direct regression test for the defect, which fails today — plus one headless session where the local player equips (via the bot command surface) and crosses a boundary, asserting the same, with cause=headless-attach/propagate probe lines in the log.

8. Budget and stop conditions

Size estimate at HEAD (supersedes the scoping's §7.6 table, whose shape changed twice — the propagation research added D2/D3, and D6 became a deletion):

piece non-comment production lines
D1 attach re-cell in the cell-less commit 40-80
D2 directory-funnel propagation + recursion/idempotence guards 50-90
D3 delete/EndGeneration explicit edges 15-35
D4 demotion + internal presentation-only child entry point 40-80
D5 headless parent-realize drive 60-110
D6 ClassifyLeaveWorld family deletion net 60 to 70
D7 reorder ~3
probe 15-25
net added ~165-355

Within the scoping's 300-490 envelope (below it, thanks to D6 being a deletion). Tests are the larger share, ~450-750 lines.

Route 7 remains ONE slice and MUST NOT be split — re-validated at HEAD: the Runtime canonical write (D1/D2) and the App demotion (D4) are two halves of one transfer. Landing D4 without D1/D2 leaves every equipped child cell-less/stranded (the #184 shape); landing D1/D2 without D4 creates a per-frame two-writer race on the canonical cell — the exact defect class this campaign exists to remove. D5 rides along because it is the same Runtime commit with a thin driver, and the headless gate is the route's direct regression test.

Stop and report rather than pushing through when:

  1. Added production lines exceed 550 — the likely cause would be the propagation needing its own publication/receipt machinery (P8 flipping the default into something structural) or the headless resolve needing more of the graphical protocol than the thin drive assumed; either is a decomposition conversation, not an ad-hoc build.
  2. Any placement, park, service-window, or ConstrainTo machinery starts looking necessary on this route (D8's last paragraph).
  3. P8 finds a Rebucketed consumer that genuinely needs per-child deltas AND publishing them breaks an ordering invariant.
  4. P4 finds a live child broadphase registration that per-crossing propagation would leave stale (that would mean acdream has child shadow state retail does not, and the design changes).
  5. D5's validation gap turns out to require extending the prepared-content bake format.
  6. Any surviving classifier test changes expectation (§3 item 10).
  7. The complete Release suite deviates from baseline beyond the two named flakes.

9. What this slice does NOT do

  • AP-131 (shared merge call / clearParent gating) — C5. The Position-unparent edge's ordering inversion (TryApplyPosition:1830/:1838 vs retail @0x00454129) is the same family: recorded here, not touched — it belongs with AP-131's route-4-side correction.
  • AP-135, #276, #316 — untouched.
  • R6-a (retail DeclareValid's SetSelectedObject split-recovery selection transfer) — out of C4, per route 6's closure; file separately.
  • UnparentBeforeRouting / ApplyPlacementFrameBeforeRouting stay recorded-not-consumed (4b-3's non-goal, carried).
  • No child PositionManager/interpolation/RemoteMotion machinery; no child self-simulation of any kind.
  • No changes to routes 2/4/5/6, the local-player paths, the remote tail, or the continuation executor beyond the parent-replay path already calling the extended commit.
  • No re-litigation of AP-133 (ParentCellId/EffectCellId split) — the child's render-parent field remains presentation.
  • Route 3 (portal) — after this slice.

10. Stale and false scoping claims — reported, not smoothed (§7 of 2026-08-04-c4-routes-6-7-scoping.md)

Substantively false or superseded (5):

  1. T5's open question — "whether retail re-cells a child when its parent crosses a cell is NOT established" — is SETTLED (yes, recursively, from the physics commit) by the propagation research, which the scoping demanded before demotion. Superseded, exactly as the scoping asked.
  2. §7.4's fix shape — "move the parent-cell commit into TryCommitParent / CommitAcceptedParentCellless" — is INSUFFICIENT as written. An attach-only commit is correct at attach and stale from the parent's first crossing (research §10 item 2). D1+D2 replace it: attach half PLUS the sustaining propagation. The scoping's own §7.6 budget row inherited the insufficiency.
  3. §7.6's "retail's change_cell + recalc_cross_cells half" — porting recalc_cross_cells per the commit is WRONG: retail runs the recursive form at attach only; per-move it calls only the non-recursive forms (research §8, binding trap). No cross-cell/shadow rebuild ships.
  4. §7.5's lean ("wire ClassifyLeaveWorld") is overridden with cause: T3's verification (no pickup/parent gate measures a teleport pair; the only retail gate is POSITION_TS, already enforced in InboundPhysicsStateController) plus retail's separate-wire-handler dispatch make deletion the evidence-backed choice (D6).
  5. §8's "recommended order: route 6 first, then route 7" and the campaign-plan correction it demanded are MOOT — both landed (1b484937; plan lines :97-116 corrected).

Stale line references (every RuntimeEntityObjectLifetime citation, plus several others): TryApplyPickup :1226-1245:1249-1312; TryCommitParent :1360-1374:1388-1441; CommitAcceptedParentCellless :1377-1408:1443-1474; CommitWithdrawal :1845-1860:1896-1929; TryAcceptDelete :1939-1952:1973-2034; ForgetInitialCreateResidence :2552-2569:2620-2637; AcknowledgeProjectionAndPublish :2187-2213:2255 on; ClassifyLeaveWorld :475-505:480-510; ValidCreateAuthority :507-512:512-516; TickChild rebucket :405-408:406-408; RebucketLiveEntityPresentationOnly :993-1050:993-1065; App wrappers :2280-2312:2312-2363 (doc comment :2288-2292:2338-2343); headless OnParentUpdated :313-317:312-316; OnPickup :455-474:460-474. The scoping's §7.3 verification table (cancellation choke points :1074-1094 etc.) is wholly re-verified at the new locations in §1.5. Its structural claims all still hold; only the coordinates moved.

Confirmed still true at HEAD: ClassifyLeaveWorld has zero production callers; RebucketLiveEntity is not presentation-only (canonical CommitRebucket at :904-907); headless has no realize; the six cancellation choke points and their C0 fixes; T8's pinned LeaveWorld omission; LiveEntityDeletionController purely logical; the register's AP-124 status.


11. Cross-contract finding — route 7 changes route 4b-3's cell-less trigger population (reported honestly)

Route 4b-3's connected gate recorded an honest gap: cause=cellless was never observed live, and its closure note says "the unwield-to-3D path is the cheapest reachable trigger" (2026-08-04-c4-route-4b-3-contract.md, final section). After route 7 that provocation stops working, and that is the retail-faithful direction:

  • Retail: unset_parent performs no cell work, so a wielded child's unwield Position reaches MoveOrTeleport with this->cell = the parent's cell — NON-zero. Retail's cell-less branch does NOT fire for unwield; it fires only for genuinely never-celled/withdrawn bodies.
  • acdream today: a parented child's canonical cell is whatever the render-tick writer last produced — nonzero in the graphical host while TickChild runs, zero headless and zero in any pre-first-tick window — so PreMergeCommittedCellId == 0 (the 4b-3 D1 input, measured at TryApplyPosition:1801-1814) could classify an unwield as cell-less.
  • After D1/D2: a committed child's pre-merge cell is deterministically the parent's (nonzero whenever the parent is celled), so the unwield Position classifies by TELEPORT_TS/distance — matching retail's predicate population exactly.

Consequences to carry: (a) 4b-3's test 4 ("unwield-to-3D shape classifies SetPosition") remains valid ONLY as a synthetic pre-merge-cell-0 fixture — it must not be re-labeled as the live unwield behavior; (b) the recorded live-closure recipe for cause=cellless needs a different provocation (a genuinely withdrawn body receiving a Position without an intervening Create — whether ACE ever emits that shape is unestablished); update the 4b-3 contract's closure note in this slice's docs commit rather than leaving a recipe that can no longer fire. No code in the 4b-3 arm changes.


12. Open questions routed to the reviewers

  1. D2's chokepoint placement (retail-conformance + architecture): the directory funnel is argued from §0 item 8's caller closure — verify independently that no canonical cell write bypasses RuntimeEntityDirectory.SetFullCell/RefreshSnapshot at HEAD (the load-bearing claim; if a bypass exists, D2 has a hole exactly where the defect class predicts).
  2. D3's delete edge: confirm by reading the App teardown/orphan flow (EquippedChildRenderController's _pendingOrphanRemovalByChild, LiveEntityRuntimeTeardownController) that zeroing children's cells at parent delete cannot race a child projection teardown already in flight, and that the child's later own-DeleteObject converges.
  3. D4's entry point: confirm the presentation-only child rebucket cannot be reached for a non-child record (the general-bypass hazard) and that BeginPresentationOnlySpatialMutation's guard semantics hold for the per-frame cadence.
  4. D5's validation gap: confirm no existing prepared-content surface exposes Setup.HoldingLocations before accepting the register row; and review what the headless drive deliberately does not drive.
  5. P8's publication decision: adversarially hunt a Rebucketed consumer that needs the per-child deltas the demotion removes (route 5's A1 class — the App tolerating the seam's silence must be shown, not assumed).
  6. D7's inertness argument — verify no observer distinguishes the reordered pickup sequence (the claim is argued, with the T7 history, not merely asserted; but it is an ordering change on a live path).
  7. §11's 4b-3 interaction — confirm the synthetic fixture reading and that no OTHER consumer of PreMergeCommittedCellId changes population when children stop being cell-less.