Spawn
playmakespawnjam
⌘K
visitorclaim namesign in
sign up
playmake
spawn / aboutwhat we're building

pinned

start herewhat spawn isfaqfrequently asked questionsthe betthe spawn bet

updates

engine v6.0One PlaceSep 5, 2026engine v5.2LumeJul 10, 2026engine v5.1ConnectionJun 18, 2026engine v5.0For RealJun 4, 2026engine v4.6AtelierJun 1, 2026engine v4.5Surface TensionMay 22, 2026engine v4.4SolidMay 15, 2026engine v4.3GroovyMay 13, 2026engine v4.2ContinuumMay 9, 2026engine v4.1FoundationsMay 4, 2026engine v0.1GenesisApr 29, 2026

pinned

what spawn isstart herefrequently asked questionsfaqthe spawn betthe bet

updates

One Placeengine v6.03d agoLumeengine v5.22mo agoConnectionengine v5.12mo agoFor Realengine v5.03mo agoAtelierengine v4.63mo agoSurface Tensionengine v4.53mo agoSolidengine v4.43mo agoGroovyengine v4.33mo agoContinuumengine v4.24mo agoFoundationsengine v4.14mo agoGenesisengine v0.14mo ago
← All posts
← All posts

engine v5.2.37

Engine v5.2.37

September 1, 2026

A patch in the Lume line.

Rebuilding and repairing mined areas works again, rapid Savi edits no longer hitch big games, gravity changes apply instantly on every physics engine, objects no longer freeze after Savi's builder briefly hosts your room, and sliders work in creator-tab panels.

what's new

  • Sliders and draggable controls work in creator-tab panels. A native slider (input type=range) in a creator tab built with onMount never responded to dragging — buttons worked but thumbs stayed put. Panels in creator tabs now treat every press as panel input, so sliders, drag handles, and text selection inside them behave like normal UI, and presses inside an open panel no longer leak into the world as camera or selection input.
  • Savi is now told when a model's collider does not match what she asked for. Declared collider sizes that get replaced by an auto-fit shape, and model colliders that never materialize, produce a clear message instead of silent surprise — so Savi stops guessing why rays pass through a body and tells you what actually happened.
  • Changing a world's gravity takes effect immediately on all physics engines. Asking Savi to change a place's gravity mid-session (updatePlace with a gravity value) now applies to falling and resting objects on the very next physics step, whichever physics engine the place runs — including objects that had settled to sleep, which wake into the new gravity. Before, places on the default engine kept the old gravity until the place fully reloaded.
  • Purchases that award a spin now work. A purchase whose fulfillment applies angular velocity or an angular impulse (making something spin as the reward) was refused with a generic error; those verbs are accepted now.
  • Objects no longer freeze after Savi's builder briefly hosts your room. When your tab stalled for a few seconds (a debug dump, a heavy save), Savi's background builder could legitimately take over simulation, and some physics objects it touched stayed assigned to it after your tab came back — they hung mid-air or refused to move until the room was fully reset. Those assignments are now cleaned up the moment your tab takes the room back.
  • Rapid Savi edits stop hitching big games. In games with a lot of scripts, every script save caused a visible stutter while the whole script record was re-scanned; now only what actually changed is re-processed, so back-to-back edits feel smooth.
  • Repairing a mined area by re-running the same fill works again. Re-running fillVoxels with identical arguments over a spot where blocks were mined or broken one at a time silently did nothing — the holes stayed. Now the repair fill lands; existing worlds heal on the first repair.
  • Rebuilding with the same block type after clearing an area works again. Placing a block back in a spot it held before a multi-block clear (an explosion, breakVoxels, a fill) silently did nothing for that one material — other materials worked, which made craters feel cursed. Existing worlds heal on the first rebuild; no workaround needed anymore.
  • Savi's camera shots now show lamp, torch, and other local lighting the way players see it — before, a posed shot could show pitch-black rooms that glow correctly in the game, and Savi would try to fix lighting that was never broken.
  • Savi's camera shots also now show characters and animated models the player wasn't looking at — before, a shot aimed away from the player's view could silently drop them, and Savi read your models as missing when they were standing in the world.
  • A script that accidentally re-attaches an object under itself every tick can no longer crash the whole game world — the engine now refuses the bad attach immediately and tells the script exactly what went wrong and how to fix it.
  • Uploaded models whose source file carries a large built-in offset no longer vanish from the game while every measurement says they are standing right there — they draw, animate, and cast shadows at the spot you placed them.
  • Leftover copies of departed characters (like Savi's builder popping in and out while she works) no longer pile up on screen on slower devices — the cleanup that removes them now runs even when your device is busy.
  • Sharing a game whose whole presentation is HTML UI (menus, cards, text screens) now posts a screenshot of what players actually see — before, the shared image was solid black.
  • Players in games without gravity no longer drift away endlessly after leaving a moving platform — the platform's momentum now stops after a few seconds instead of carrying them forever.
  • Texture scripts, material scripts, look scripts, and voxel builders can now read data from .json files with require(), the same way behavior scripts always could — a texture that pulls its palette or lore from a JSON data file now bakes instead of failing to compile.
  • Savi no longer piles up copies of her body in your game. Each time she started a piece of work, a fresh copy of her builder joined your room and bowed out a minute later — and on loaded devices the departed bodies could stick to your screen until reload, dragging the whole game. Her builder now keeps one identity per room, so there is only ever one of her body, however busy she is and however loaded your device gets.
›technical notes
  • The creator lifecycle mount carries the UI-plane marker (ledger 2822, savi filing 62e40112 — Mobieverse, 5.2.36). Row 2437 stamped data-input-capture="ignore" on the lifecycle mounts so the overUi verdict reads mounted panels as UI, but the fix landed on #tome-game-mount only — #tome-creator-mount (the sibling branch in realm/frame/lifecycle.ts) stayed unmarked. The creator mount is pointer-events:auto and owns the viewport, so every element in a setCreatorTab onMount panel was hit natively while isOverUiTarget() read false, and input-forward's handlePointerDown preventDefaulted every press as the world's. Cancelling pointerdown cancels the mousedown default action, so native drag surfaces — an input type=range thumb — never dragged, while the spec keeps compat click alive: buttons worked, sliders dead (the filing's exact felt split). The presses also forwarded overUi:false, so god-mode camera/select received every panel press as a world press (double delivery — the same disease 2437 cured on the game mount). The marker now stamps on both mounts in one shared line; the creator mount is also thereby excluded from the interactive grammar as an engine UI plane (row 2527's law — its inline pointer-events:auto no longer reads as a script-wired control) and becomes visible to the realm DOM read census (frame/read.ts walks [data-input-capture="ignore"] subtrees, which had silently excluded creator panels from Savi's UI reads). Two-copies sweep receipt: the frame document holds exactly three engine planes (render container, game mount, creator mount) and all three now carry the marker; every host-document plane (chrome-host container, touch chrome, stick visuals, savi-todo cards) already carried it; the renderer debug inspector family uses its own stopPropagation shield by design. Red-then-green pinned in lifecycle.test.ts + input-forward.test.ts (mount built by the real lifecycle host: range pointerdown/mousedown defaults survive, panel-background presses read overUi:true, no-panel body presses stay the world's) and reproduced red-then-green in the real product (creator tab open, real mouse drag: thumb pinned at 20 before, drags to 92 after; game-mount marker unchanged both sides).
  • Dropped collider dims and unrealized model-mesh colliders on kinematic model bodies get named verdicts (r-04678d9d, honesty half of dig 2786). Declared physics.capsule dims on a body:'kinematic' MODEL entity silently resolved to the box-derived default pill, and collider:'auto' on a model could yield no live collider at all when the mesh cook never realized — the only trace was a ring-only warn invisible from chat on relay rooms. Two verdicts now ride the client-reported behavior-fault channel (ring+DM on notifier worlds, engine.diagnostic wire on clients), once per entity per session: the apply seam names declared capsule dims that were dropped and the auto-fit shape actually in effect; the realization seam names an asset-backed mesh collider still unrealized past a 15s grace window. SIZING IS UNCHANGED — the verdicts are the whole diff; whether kinematic bodies should honor declared dims rides the API-taste bench.
  • Rapier joins mantle's per-step gravity re-read (r-f79ef781, bench 4cef4a31 §4a). Two physics backends held two contracts over one spec field: mantle re-reads the place's gravity at the top of every step (stepMantlePlace → resolvePlaceGravity → MantleWorld.setGravity, compare-then-set + wake-on-change), while rapier read it once at physics-runtime creation — so a template-level updatePlace({ gravity }) was live on mantle and dead on rapier until the runtime happened to rebuild (mode flip, corruption, place reload). stepPlacePhysics now resolves the place's gravity every step and applies it through syncPlaceGravity (rapier/sync.ts): setRuntimeGravity (rapier/runtime.ts) compares in solver space and mutates World.gravity's fields in place only on change — rapier's World.step reads that plain JS property fresh on every call, so the write is the whole apply (no rebuild; steady-state per-step cost is the same resolvePlaceGravity call mantle already pays plus float compares, no further allocation). 2D worlds project the spec's 3D gravity into solver coordinates exactly as construction does (2d-top simulates world z as the solver's y). On change, every dynamic wakes — mantle's setGravity wakes all live dynamics for the same reason (a slept body never re-tests a world-wide force change), and since sleep is authoritative (PhysicsBodyState.status, replicated), the replicated bit clears in the same write per the clearReplicatedSleepStatus contract; the site is mode-both/deterministic, so both realms derive the same wake. Pinned red-then-green through the real seams (ObjectAPI updatePlace → the PhysicsEngineFeature physics/step system) in live-place-gravity.test.ts: live-within-one-step on rapier 3d, rapier↔mantle per-tick-acceleration parity under the same write, slept-body wake into inverted gravity, and the 2d-top z→solver-y projection. The instance-write half of the filing (updatePlace refusing runtime instance ids) is deliberately untouched — it is the instance-family seam, deferred by the bench to the instance-write family review.
  • The deferred-op verb allowlist derives from one roster (r-1f80ff12). OBJECT_API_DEFERRED_VERBS (exec/transaction-log.ts) is now the single source for the DeferredOp union, queueObjectApiVerbOp's parameter type, and purchase-fulfillment's runtime allowlist. The hand-kept copy in purchase-fulfillment.ts was born against a stale union: setAngularVelocity, setObjectAngularVelocity, addAngularImpulse, addObjectAngularImpulse were queueable as deferred ops but a purchase fulfillment naming them refused with the generic purchase_fulfillment_invalid. A verb now joins by one roster edit; the roster is pinned against the live ObjectAPI surface so a renamed or removed verb reds the build naming the stale entry. Verbs outside the roster still refuse — the trust boundary stays a deliberate physics-shaped subset.
  • A standing shell's sim-lease rows die with its seat, not its socket (ledger 2811, dig 5767ff3e). The relay tier's departed-holder lease sweep (sweepRelaySimLeaseDepartedHolders) enforced "leases die with their holder's claim" only for the departed-holder shape: it dropped rows whose holder had no live m:* membership head, and it ran only at membership edges. The headless builder shell (tokenClass room_host) broke both halves at once: during a creator-tab stall the relay honestly seats the always-beating shell, the shell's tenure mints lease rows (rag proxies, loose dynamics) adjudicated at its own seat, and when the creator's beat returns and the seat flips back, the shell is still a live, beating member — the departed rule never fires, no membership row rides the seat flip, and the rows survive in every world. A leased-to-shell entity is then frozen at the creator (leased-to-other excludes it from host simulation; no lease names the creator; the shell no longer simulates the place), the lease row's presence makes the shell the entity's standing authority (it refuses the creator's re-claims — the lease_contested face in the 2811 tape), the keyed lease:<entityId> journal head re-folds the residue into every fresh join (reload does not heal), and only a journal reset re-minted the room. Three moves: (1) readRelayMembershipRow surfaces the m:* head's identity.tokenClass (journaled bytes — fold-pure; lenient like every advisory rider, and pinned against the real relay DO in cf-relay's room-truth workerd suite); (2) the world-side membership fold tracks standingSessionMembers beside members; (3) the sweep drops a live member's row when the holder is a standing session that does not hold the seat of the leased entity's place (TomePlaceHosts), and the election's hosts-table write (runtime-worker writeHostsTable glue, harness parity) becomes the sweep's SEAT edge. Real players' rows are deliberately untouched — leases exist precisely so a non-host client simulates what it touches; only the standing-session shape is seat-bound. Absent hosts table or unresolvable place keeps today's behavior (mid-fold conservatism). No flap fuel: the sweep only deletes rows, so a shell that legitimately re-wins the seat re-mints through the ordinary claim door (epoch floors monotonic). Pinned red-then-green in relay-sim-lease.e2e: live-but-unseated shell row swept at the seat's return (a body-arrival flip with no membership edge), current-seat-holder shell row survives, departed-holder sweep un-regressed, clean re-mint both directions across a seat flap, and a late joiner behind the journaled residue head folds clean at fold-complete.
  • Script-save cache eviction is surgical (ledger 2794, dig 2c099300). Every Savi script save cleared the compiler's content-keyed caches wholesale (invalidateBehaviorCache → clearSourceContentCaches) and the spec-apply fold then synchronously re-dep-scanned and re-hashed the game's entire script record on the client main thread — O(total script bytes) per save regardless of the change (Brushfire's 1.65MB of scripts made every save a ~30-90ms felt hitch, sim shed counters climbing in step). The spec-apply lane holds both records at detectChangedScripts, so eviction now drops exactly the superseded source strings; every still-live entry stays warm and the next apply re-scans only the changed content. The memory-retention bound the wholesale clear enforced is kept exactly (a superseded string dies on the save that supersedes it, pinned by a retention test through the real applySpec lane); the scriptHash domain moves to its own small wholesale-cleared cache because its derived-string keys cannot be named by eviction. Callers that cannot name superseded strings keep the wholesale clear. Bench (real Brushfire spec, medians of 7): fold with a 1KB script edit 35.3ms → 5.6ms; 209KB lib edit 42.3ms → 7.0ms; delta-0 unchanged.
  • The identical-fill dedupe now also yields to later single-cell commands inside the fill's box (ledger 2817, second half — the adversarial review's known-miss on the first fix). appendRegionEditToState's identical-fill dedupe matched the last same-bbox voxel-set command and stood down only for later REGION-shaped commands, so an identical region fill re-run over cells that later single-cell commands rewrote (fill floor → player mines a block → repair script re-runs the same fillVoxels) silently returned false and the hole stayed. The dedupe now stands only when the matched fill still wins every cell of its box: any later region-shaped command (position-blind, as before) or any later single-cell command whose cell lies inside the box makes the identical fill re-land — removed by the filter, re-appended at the end, journal length unchanged. Later single-cell commands outside the box keep the standing no-op. Cost: the shadow scan is bounded by the commands after the matched fill (at most the chunk journal's length, and it runs only on the identical-payload path); churn is bounded at one fill re-land per intruding single-cell write and one single-cell re-land per region append (cross-funnel interleavings pinned in voxel-region-shadow-rewrite.test.ts, including the review's P1/P3 probe shapes), and the journal never grows. Pre-fix persisted journals heal on the first identical re-fill, compacted or not; the id-reuse gate and the replay guard are untouched.
  • The voxel journal's identical-write dedupe consults region shadowing (ledger 2817, dig 6403056f). applyVoxelTerrainEditToChunkEditsState matched the cell's last single-cell command in index.byCell and returned null on identical material+state without asking whether a later region command shadows that match — so place-dirt → region-clear → place-dirt silently no-oped, and the cell stayed unwritable for that one material until a different-material sentinel retired the stale command (the journal's own per-cell contract is "later commands win"; the append path twenty lines down already checked existingIndex > lastRegionIndex). The dedupe now stands only for a match past the last region-shaped command; a shadowed match re-lands through the existing shadowed-command branch — replace and move to the end, exactly like a changed value — so the journal keeps its length and automata churn stays bounded (one re-land per cell per region append, then the dedupe resumes). A shadowed author-minted duplicate (same id, same content — the at-least-once delivery shape) stays a quiet no-op and never re-lands or trips the id-reuse gate. appendRegionEditToState's identical-fill dedupe had the same blind spot for the fill funnel (a 1-cell fill shares the stale place's bbox, which is why the filing's 1-cell fill also refused): an identical fill now dedupes only when no later region-shaped command follows the match; single-cell commands after the fill keep the standing no-op. The poison was durable — compaction preserves everything at or before the last region command verbatim, so persisted journals re-armed the refusal on every load; journals persisted before this fix heal on the first write of the affected material (pinned in voxel-region-shadow-rewrite.test.ts, red-then-green through the real ObjectAPI funnel plus the serialize+compact+deserialize round-trip).
  • Posed captures re-cluster lighting for the capture pose (ledger 2837). A camera/frame capture re-poses its own camera but was handed the LIVE frame's clustered-lighting bind group; clustered lighting is a view-space froxel grid, so every capture fragment sampled the PLAYER camera's clusters and point/spot lights near the capture pose rendered black (the row's A/B: viewport shows bonfire light pools, the same-moment posed capture shows a black wall 3m from the fires — three build lanes wasted verification budget fixing frontages that were never dark). The capture now writes cull params for ITS pose and re-dispatches the cluster-cull compute on its own graph before its render pass (lighting/contributor.ts captureClusterCull, the frustum-cull re-fit's exact restoration contract: the next live prepare rewrites the params and re-culls for the main camera, so nothing persists). The studio-chat confession lifts with this version (capture-lighting.ts, version-gated on the capture-cull template).
  • The models lane re-fits its CPU draw verdicts for the capture pose (ledger 2853). Skinned characters, horde packs, and scripted models verdict their draw gates on the CPU against the MAIN camera during prepare, and captures replayed that verdict — a capture posed at content the player was not looking at drew none of it (sixteen valid skinned GLBs on a plinth read as missing while standing in the world, on an engine whose GPU cull re-fit was already live). The models contributor now implements captureRefit (the sprite lane's ledger-2408 contract): character visibility re-verdicts, hordes and scripted models re-pack against the capture camera, without ticking animation or touching the live cross-frame facts; the next live prepare restores the main-camera fit by construction.
  • identify_object speaks view-culled for models (ledger 2853's verdict half). A CPU-verdicted model (skinned/horde/scripted) outside the live camera's frustum or draw distance now answers hidden: { reason: "view-culled" } with real bounds instead of a silent "rendered" — the same verdict sprites already carry. Static batches stay verdict-free on purpose: their frustum verdicts are GPU-resident and the CPU must not guess. The three CPU lanes also adopt the sprite lane's mask-era law (a mask-hidden tick writes no sphere claim), so a mask fix can never read a stale mask-era bit as "off-camera".
  • The hierarchy depth-16 guard now REFUSES at the attach seam instead of warning after the edge lands (ledger 2867). A bench script re-parented an audio entity under itself per tick; spawn's id qualification compounded the entity path every cycle (doubling to 159M-char ids), the solve's warn-only guard fired 392 times while the attach kept applying, and the room host OOMed at 33G. One guard (tome/hierarchy-guard.ts) now answers every attach: a re-parent cycle (self or own-descendant) and any attach whose deepest node would cross depth 16 refuse with a teaching error the script feels — setParent/attachTo keep the pre-attach parent and log the verdict on the exec/log rail; a parented spawn refuses WHOLE (nothing minted), because a refused attach that still minted the entity would hand the next cycle a fresh, longer parent id. The depth refusal names re-parent-loop evidence when the path itself repeats a segment. The spec rebind's silent self-parent skip now falls through to the same loud refusal. Cost on the hot lanes: one upward walk bounded by the cap itself (≤17 component reads); subtree height is O(1) for fresh spawns and level-bounded by the remaining depth budget for re-parents.
  • A log line never carries an unbounded id: entity ids in [Tome] warn/error lines are bounded at 256 chars with a …[+N chars] marker (boundLogId), whole runtime-log messages and tomeLogger warn/error strings are belt-bounded at 16K (the incident's own warn lines were the memory amplifier — each printed the entire 159M-char id).
  • CPU model culling centers on the bounds center, never the world origin (ledger 2857 — the mage-giant invisibility). A GLB whose SPAWN_POSTPROCESS_ROOT bakes a large root translation (T.y ≈ 118 in the filing's A/B) puts its rest-bounds center far from its node origin, and the feet-pivot ground alignment (groundOffset = −bounds.min.y, riding geomScale) drags the visual's world ORIGIN the same distance the other way — ≈ 853m at the game's entity scale 7.22. The CPU cull query centered on that origin, and isSphereCulled's distance verdict measures to the query center with no radius credit, so the model was distance-culled everywhere in its own arena, shadows included — "renders NOTHING client-side" — while identify_object, raycasts, and getWorldBoundsBox stayed exact (those are AABB/BVH-based, center-aware), so no instrument could name the fault. Characters, hordes, and scripted models now cull on the world-transformed bounds CENTER with the half-diagonal (+ morph pad) radius (models/cull-sphere.ts), exactly like the static-batch GPU kernel always did; the character caster sphere and the anim-LOD distance anchor ride the same center. Verified against the filing's own asset at the filing's exact placement: the T.y-118 variant now ticks visible, uploads, and draws.
  • Fixed the entity residency sweep starving on loaded clients (ledger 2863). The sweep (entity-residency-reconcile.ts, the "orphans drawn forever" cure from ledgers 2382/2338/2376) deferred whole censuses whenever the render channel had pending frames — and a fast sim writer against a slow reader (heavy world, constrained device) holds pending frames near-always, so sim-dead ghosts were never released on exactly the clients that lose despawn frames most (field specimen: 29 dead 12.7k-tri skinned bodies drawn against a fresh census). Releases now bypass that defer: a delete frame of sim-dead ids is a few bytes and idempotent, while the defer's congestion argument protects re-forwards of entity state going out — forwards keep the defer unchanged, and one release-only pass per census keeps the per-tick cost at one boolean test. The sweep also stamps cumulative consume/defer counters onto the ECS capsule's census block (entityResidency.sweep), so a dump can tell "the sweep never got a turn" from "releases flow but the delete frames are lost downstream".
  • The share-to-Discord capture composites the authored HTML UI plane (r-0daa3503). The savi-notes capturer read only the presented 3D canvas, so a UI-only game (0 objects, ui.js IS the game) shipped a fully black image wearing the game's name — and the 2D/UI-first class is growing. The capturer now asks the game realm to rasterize itself through the capture broker (the same realm-raster + composite view_live_scene's viewport eye already ships, r-82c2f902) and layers it over the 3D frame before resize/upload; every non-composited outcome (no realm mounted, canvas-slot layout, raster failure) ships the plain frame with an honest note, and the parent notification carries the uiIncluded/uiNote verdict so the share modal states what the image saw instead of guessing from pixel brightness. The kiln modal's black-frame confession (#13729) stays as the belt.
  • Fixed stand-parent's post-detach ballistic carry never terminating for riders whose motor never applies gravity (ledger 2852). The carry contract is "ballistic, no decay, until the next grounding" — but grounding requires descent, and a gravity-less rider never descends, so a latched carry was unbounded (a body sailed 190u of sky over 8.5 minutes in production). The carry now expires after 4 seconds without one descending tick, measured on the rider's own post-step world position so any gravity source counts (move(), addVelocity(), scripts). Riders that descend keep exact ballistic behavior to grounding, however long the fall — jump arcs, walk-offs, and platform-despawn momentum are unchanged.
  • Fixed require() of a .json module dying with "Unexpected token ':'" in the four worker-side dependency compilers that compiled every dep as JS (ledger 2866; production filing: repeated texture-script-compile-failed for scripts/tex-crt.js requiring lib/lore.js requiring lib/data/lore.json). The main module lane parses .json deps with JSON.parse (script-modules.ts compileModule; terrain's generator-runtime has the same branch since #5768), but the texture-bake, scripted-material, scripted-look, and voxel-template dep compilers had no .json branch, so a valid JSON body parsed as a block statement and the script parked as a compile fault. Each lane now mirrors the main lane: a resolved dep ref ending in .json compiles by JSON.parse; every other dep compiles exactly as before (a pinned non-json chain bakes a byte-identical draw-command stream). Census of the remaining compile lanes: the tome module lane and terrain generator-runtime already carry the branch, vibe scripts take no lib deps by design, and remesh compiles no script modules.
  • The standing builder shell wears ONE identity per room (ledger 2863, dig b2b659a7). Every headless-shell boot used to mint a fresh account-less host-session:{uuid} subject, so the chat DO's summon treadmill (resummon on savi activity, etiquette-yield ~a minute later under a creator-held seat — 67 boots in 69 minutes on the field specimen) minted 67 distinct savi-dressed player entities in one session, and every despawn frame a loaded renderer lost stood as a permanent ghost (29 of 67 on the specimen's tablet — 29 skinned 12.7k-tri characters dragging the device). The anonymous hostSession mint now accepts a sessionSlot: a slotted mint derives its subject from (variantId, roomId, slot) — host-session:room-{slot}-{hash24} — so every boot of a one-per-room seat wears one identity. The player entity id (player/<identityId>) is therefore identical across boots, and a maximally lossy renderer can hold at most one savi body: duplicates unrepresentable at the id level. A fresh boot supersedes any straggler socket through the tiers' existing same-identity takeover (cf-relay §2 supersede — already documented there as "the intended takeover" for shells; the container tier's clientId-keyed connection table), the straggler exits without journaling a despawn (the superseded path never authored one) and cannot outrace the successor (journal writes ride the closed socket), and a mid-session token re-mint returns the SAME subject, so the old "a re-mint changes the clientId — a self-supersede" hazard is structurally gone. The chat DO's supervisor sends slot builder; spawn client walkabouts and probes stay slotless per-boot (their side-by-side concurrency in one room is legitimate — spawn client join additionally strips an ambient slot from its child env, so a walkabout can never supersede the standing builder). The lifecycle law (p-6c534b0f leg 2: deliberate exits journal their own despawn before socket close) and the summon cadence (ledger 2811's aug-12 ruling: presence never blocks a summon) are untouched — this changes identity only. Membership churn drops too: a same-identity rejoin over a live m:* head skips the re-mint (the ledger 2660 gate), and supersede tombs nothing.