engine v5.2.4
Engine v5.2.4
July 19, 2026
A patch in the Lume line.
what's new
- Fixed top-down (2d-top) characters colliding ~1m south of where their sprite stands: walking into a wall from the north stopped short, from the south overlapped, and the debug collider circle drew below the character's feet. Colliders now sit exactly at the sprite's feet regardless of the object's rotation.
- Camera scripts can now say exactly who owns the view: declare
orientation: { source: "script" }on a custom camera and the screen shows precisely what the script authors (death cams, cinematics, forced framing — even while reading the mouse), orsource: "look"for engine mouse-look at your declared sensitivity withgetViewAngles()to read back what's on screen. Existing cameras are completely untouched — nothing changes unless a camera declares. - Glowing models are back: materials using
glowTexture: true(an emissive tint that glows through the model's own texture) render their glow again instead of a flat solid color — including rigid attachments on rigged characters, which now glow in the same tint as the rest of the model. If you putglowTextureon a model with no texture, on a scripted material or scripted part, or on a part that also wears its own texture, the engine now tells you why nothing glows instead of staying silent. - iPhones opening a game they hadn't touched yet could get permanently marked as "slow devices": iOS slows the page's drawing until the first tap, the engine misread that slowdown as graphics overload, cut visual quality to the floor, and remembered the floor for every future visit. It now recognizes browser-limited frame delivery — even the deep kind — keeps the quality you actually earned, and never lets that state write itself into the device's memory.
- For devices that were already stuck at low quality by the old behavior: recovery is gradual by design — stored quality decays over the 7-day unconfirmed lease and accrued boot cuts unwind one per session, so a previously-affected device climbs back over its next few sessions rather than instantly.
- Worlds now load in whole scenes instead of objects popping in one at a time.
- God-mode clicking is more predictable: invisible helper objects no longer steal your click from the visible thing under the cursor, and parts glued to a character's bones no longer half-select.
- Long trails and heavy effects stop cutting out mid-stream in effect-dense worlds.
- Path glows and other fading trails now fade all the way out instead of leaving a faint ghost.
- Games can now upload images at runtime and use them as real assets (player-drawn art, generated posters, photo walls).
- Objects spawned with a
lifetimenow expire on schedule even if the player whose behavior spawned them disconnects or refreshes mid-lifetime — no more leftover markers, projectiles, or effects haunting a room until it restarts. - Slow, precise camera look — orbiting an object while keeping it centered, fine aim adjustments — no longer stutters against the frame rate, especially on trackpads and high-refresh displays. Fast flicks are untouched: full speed, no added latency.
- Invisible data-holder objects (like a leaderboard's storage object) now show up in tag searches again — no more "No survivors yet" over a board that actually has scores.
- Scripts that chain thousands of
.add()/.mul()steps onto actx.paramsvalue in a loop now keep that param live-patchable — changing it updates the material in place instead of silently rebuilding the whole shader every time. - Scripts that build very long material or look chains in a loop (thousands of
.add()/.mul()steps) no longer crash the material rebuild with a "Maximum call stack" error — they compile and draw like any other material. - Single-player save systems now actually save your game: auto-saves, load-on-join, and save-on-exit hooks run inside the world you're playing instead of a stale server copy — no more progress resetting to an old snapshot after you close the game, and no more live sessions getting wiped back to a first-quest state by a reconnect.
- Single-player saves stop fighting a ghost: auto-save code in your behavior scripts used to also run in a hidden stale copy of your world, which kept overwriting the real save with day-one data. Now only the world you're playing in writes your storage.
- Walking and moving character sprites no longer ghost or smear under the high-quality upscaler — sprites now tell the renderer exactly how they moved each frame.
- Fixed
getTerrainHeight()(andgetTerrainNormal()) sometimes answering from the lower-detail terrain a device happens to be rendering — on stepped-down devices hilltop heights could read several meters low, so scripts placed or "corrected" objects into the ground. Height reads now always answer the true authored terrain, matching wherey: { terrain: N }placements actually land, on every device. - A physics engine crash inside a game now recovers by itself within a second instead of leaving the game's physics frozen (objects stuck in place, collisions dead) until the player reloads the page.
- Older worlds get their object sizes, attachment sockets, and model parts filled in way more reliably now — the game figures them out right in the host player's browser using the same access that already renders the models, instead of a server that sometimes couldn't reach them.
- Sprites' sizes, ground anchors, and collision shapes stay perfectly consistent for everyone in a room, and load a bit faster on return visits.
- When a physics shape truly can't load (broken or removed model), Savi now gets one clear message about it instead of the engine quietly retrying forever.
- Grass and decorations no longer grow-shrink-grow repeatedly while a world first loads — the grow-in now plays exactly once per reveal, and far terrain streaming in never re-grows the field around you.
- Timers set with
runInSeconds/runInTicksno longer vanish when an object's simulation briefly moves to another machine (alt-tab takeovers, host changes, reconnect grace) — they hold and fire as soon as the machine that set them is back in charge. Timers set by a player who then leaves still die with their tab: for deadlines that must survive that, use spawnlifetimeor a target tick in state. - Big built worlds got dramatically faster: the engine's per-tick bookkeeping for object hierarchies, world hashing, and script writes now scales with what CHANGED instead of how big your world is — a 7,000-object world that was spending its entire frame budget on bookkeeping (and freezing ~150ms every time a script wrote something) now pays almost nothing for the same edits.
- First-person held items are real now: attach a gun, tool, or hands to the camera (
feetPosition: { attach: "camera" }) and it rides the view with zero lag — full 3D models and meshes, not just sprites. Addmaterial: { depthTest: false }(models honor it now too) and the held item never clips into walls. Savi knows the whole recipe: aim-down-sights, bob and sway, per-player viewmodels. - "Press E" prompts stop haunting you: interact prompts hide when their object is invisible, gone, or already in your hands — no more permanent "Take" prompt floating on the gun you're holding.
- Material scripts that displace a surface (mountain relief, painted planets) can now read the surface normal while doing it — those looks compile and draw instead of falling back to a bare white shape.
- When a look transition can't fade (scripted looks snap by design), Savi is now told at the push — with the recipe for doing the fade inside the script — instead of promising you a smooth dissolve that hard-snapped.
- Saving a script edit no longer freezes the room: the new version boots in the background while the game keeps running, then swaps in an instant.
- Savi's changes reach your game much more reliably in singleplayer: rooms with no tab open run the change on the server, a hidden tab answers honestly in ~2 seconds instead of stalling Savi's turn for 20, and work a hidden tab can't do reroutes to the server instead of failing.
- A game that can't finish joining now fails fast and loud instead of holding you on a silent black screen forever.
- Music and sound generation that hits your hourly budget now waits politely and resumes when the window lifts — and Savi is told exactly which sound is capped — instead of that audio silently dying for the rest of the session.
- Destructible worlds keep their damage across script saves: geometry your scripts carved or changed at runtime no longer snaps back to pristine while the collision still remembers every crater.
- When an image or model is blocked by content policy, you and Savi learn immediately that it was rejected (and why) instead of watching a fake "still generating…" countdown for something that was never coming — only a reword can fix it, and now Savi knows that.
- Way fewer duplicate error alerts while you build: one broken script is one report (re-armed when Savi actually fixes it, not on a timer), and a script hammering a missing object logs once a minute instead of tens of thousands of times.
- Sprites no longer vanish for the rest of the session after you tab away: stuck texture loads time out and retry, failed GPU work re-tries itself, and returning to the tab revives anything that broke while you were gone.
- When the physics engine itself crashes, the errors now say so — instead of blaming your game's scripts and sending Savi chasing a bug that doesn't exist. You and Savi both hear about it honestly, and a reload cures it.
- Flat painted ground is a real look now: terrain with a tint and no texture stops warning forever about a "missing texture" you never wanted in the first place.
- Multiplayer host handoffs are steadier: a player who never actually finished loading can no longer hold a place's authority while a fully-loaded player waits.
- Savi learned to carry run state (inventory, dice, boarding passes) THROUGH place transitions the reliable way — seeded on the travel itself — instead of storage relays that can lose it at the boundary.
- Dense top-down and 2.5D worlds with lots of depth-sorted sprites (
ySort) run dramatically faster — hundreds of trees, bushes, and props that used to each cost their own draw now share batches, with pixel-identical layering (soft-alphacutout: falsesprites keep their old per-sprite path, so their layering is unchanged too). ySorton ordinary sprites is now free: give it to everything on the interleaving layer without a performance tax.- Fast-paced worlds that spawn lots of physics props (bombs, projectiles, debris) no longer slowly lose their visual effects: entities the room silently dropped used to linger invisibly and eat effect capacity until explosions just stopped rendering — now they clean themselves up within seconds, effects included.
- A model that genuinely has no collision shape now tells Savi so once, clearly — instead of the engine silently re-asking forever and slowing down every other asset load in your world.
- Physics objects no longer twitch while rolling after bumps — a rolling ball or tumbling cube keeps its spin through network corrections instead of freezing and jerking.
- Savi now gets a log when a squash amount is out of range instead of silence.
- Savi's script edits now reliably wake up in your world — including worlds nobody has open — instead of occasionally sitting inert until a reboot.
- Saving a script edit can no longer kick everyone out of the room: one save is one seamless background swap, even when several update signals race.
- Mouse-look games with on-screen HUD panels no longer trap your cursor: click the game world and you're back to looking around, with the panel still on screen. Real menus and dialogs keep the cursor until you close them, like always.
- Savi now gets an accurate explanation when event handlers set up while spawning an object don't stick — the error tells her exactly where to move them, so she fixes it on the first try instead of being pointed back at what she already did.
- Panels you've already clicked past no longer flicker the cursor back when their UI re-renders — the engine now recognizes a re-rendered panel by its
ideven when the re-render rebuilds the element. - Night scenes and colored atmospheres finally show the color you authored: a deep navy vignette is deep navy, a 9% haze reads as 9% — dark looks stop washing out into a pale veil.
- Multiplayer places no longer freeze up permanently when the hosting player's connection quietly breaks one way — the room detects the wedge within seconds and hands hosting forward on its own, instead of needing Savi to restart the room.
- Equipment and body rigs are one declaration now: give any object tree
parent: "player"and EVERY player in the room wears their own copy — swords, boards, flashlights, full composed rigs — with no pooling scripts, no ownership wrangling, and no more "works with one player, vanishes with two." - Fixed a loading screen that could stay stuck at "Starting game…" over a game that was actually running — anyone hitting it had to be told their world was fine; now the curtain lifts as soon as the game is drawn and its content is loaded.
- Fixed a bug where games with a custom camera and no camera script (common in UI/card games) got stuck on "Starting game…" forever. They load normally now.
- Custom player models set from scripts finally obey your animations: the default avatar's built-in walk/run no longer hijacks a model your script dressed and animated — your idle/walk/run play the moment your script writes them.
- Savi's singleplayer fixes land even when your tab is closed or hidden: her script runs on the server instead of failing with "no client" — the biggest remaining class of "Savi said she fixed it but nothing happened" in solo worlds.
- Savi's teaching got truer in the corners: she now knows spawned objects must live in a resident place to replicate, that one building is one place (no more place-per-floor), how to stamp bone-mounted equipment on every player, and to let YOU be the judge of whether a fix worked.
- Tilemap tiles can animate now: stack frames vertically in a tileset atlas and set
tilesetFrames+tilesetFps— shimmering water, flickering torches, whole layers playing in lockstep. Worlds that don't use it render exactly as before. - Deleting a state key finally sticks:
patchState({ done: undefined })survives restarts, and the newapi.deleteState("cells.3,4")deletes nested keys — no more stringified-doc or tombstone workarounds for "clear". - Bulk property writes (
batchSetObjectProperties,setPositions) made in persistent scripts now save like single property writes instead of silently reverting on reload. - Cursor-followers stop freezing during movement — the pointer ray now always tracks the camera you see. Cursor rings, placement ghosts, and aim markers stay glued to your pointer while you move.
- When an art or voice budget runs out mid-build, Savi now learns the real time generation resumes — no more silently missing sprites, and no more "back in an hour" promises over a parked daily budget.
- Terrain with small repeating ground textures stops reading as an obvious grid from above: tight repeats now get the natural per-tile variation they were always meant to have, while larger hand-authored patterns (8m and up) keep their exact look. Cliff sides are unchanged.
- Passing a long digits-only string to a drawn-art texture script no longer scrambles it into a giant number — encoded pixel payloads arrive exactly as written, no letter-prefix workaround needed.
- Sound range finally means what it says: give an ambient sound a
maxDistanceand it fades out smoothly and goes fully silent at that distance, instead of carrying across the whole map.playSoundcan take amaxDistancetoo. api.getInputRay(input)now returns exactly the ray through the pixel the player saw — cursor-following objects (rings, placement ghosts, aim markers) no longer lag or land offset from the cursor while the character is moving.- Mouse button releases can no longer get lost under heavy pointer movement: a short press during fast mouse motion releases exactly when the player lets go (held actions stop,
keyup-pulsed actions fire). - Fixed a multiplayer bug where every light in the room could flicker off and back on (about a fifth of a second, sometimes twice in a row) while someone was editing the game. Spec updates that needed a full refresh used to rebuild the player's whole world; now they refresh just the game spec, invisibly.
- Rays now hit what players see:
api.raycastintersects visible sprites by default — paper enemies, sprite tiles, and decor are targetable without adding physics bodies. Billboarded sprites are hittable from every direction, exactly like they're visible from every direction. Pass{ includeSprites: false }if a ray should only see physics colliders. - Fixed billboard sprite picking testing a quad mirrored below the sprite's feet.
- Voxel edits no longer flicker back to the old state when the world is busy: placing or clearing blocks next to active automata (flowing water, busy zoo ponds) used to show your edit for a frame, revert it for about a third of a second, then re-apply it. Your edits now stick the first time.
- Player sprites keep the size your scripts give them: wearing a skin with an explicit
sprite.sizeno longer snaps to the artwork's minted height when the engine's sprite metadata arrives (the "my character grew mid-walk" bug). Skins worn without a size still auto-size from the asset's declared world height — including when a player jumps straight from an explicitly-sized skin into an auto-sized one.
›technical notes
- Rooms move from Containers to Durable Objects (#7667). The room server no longer needs an OS: the five-isolate container topology (shell, sim worker, network worker, job pool, exec worker) is replaced by a host-blind room core behind three seams (netcode drain/send, externally driven runFrame, exec/job executor) with two hosts. RoomDO is the new default host — websockets + hibernation, a write-ahead ledger in DO storage (durability before visibility), a work-gated tick driver, crons on alarms, idle save — driving a Worker Loader engine isolate that compiles creator scripts at isolate startup; script edits swap the isolate make-before-break while sockets stay open. A thin Bun container host remains as the escalation tier for rooms too big for the isolate memory cap. The multiplayer server world now mounts an explicit 50-system manifest (down from 61) — a slim relay handling lifecycle, storage/LLM/auth rails, and fan-out, with NPC/animation/IK/voxel-tick/physics-step systems unmounted by name and pinned by a manifest test. IMPORTANT: this build ships with 0% of room traffic on the DO host — the new path stays dark until the staging pilot completes; rooms keep running exactly as before until the flip.
- 2d-top capsule feet-offset Y→Z leak (ledger 1421): every feet↔center conversion now derives the feet→center lift from the SOLVER-representable rotation instead of the raw ECS quat. New primitive
projectRotationToRuntime(rapier/runtime.ts) — 3D passes the normalized quat through; 2D collapses to the solver's single in-plane angle (yaw about +Y in 2d-top, roll about +Z in 2d-side), the same collapsewriteBodyRotationalready applies on write. Previously the lift was rotated by the raw quat, so an out-of-plane WorldRotation (a flat-lying sprite pose, e.g. ±90° about X) expressed the character capsule's 0.95m feet→center lift (halfHeight 0.6 + radius 0.35) on world Z — the visible south axis top-down — genuinely displacing the body ~0.95m south of the sprite's boots at body birth (initializeBodyState) and on every external transform sync (syncPhysicsBodyToComponents→resolveTranslationFromComponents), and drawing the F3 collider ring at the same displaced spot via the view-origin fallback (resolveViewOrigin). The renderer's 2D orientation contract (lume quad-orientation) already composes only the in-plane twist of WorldRotation; physics now matches it. resolveTranslationFromComponentstakes the runtime dimension as its first parameter and projects internally. Callers updated: rapier sync (static + dynamic/kinematic transform sync), collider rebuild realign, collider-view fallback (place mode as dimension), terrain rescue feet write. The ObjectAPI door is covered too:syncFeetPositionPhysics(feetPosition/position setters,move(), the interpreter's deferred op, NPC per-tick movement) carried two raw-quat conversions of its own — the live-handle path (transient: same-script-phase queries saw the body displaced for part of a tick) andreconcileUnmaterializedFeetWrite(persistent: a feet write in the disposed/respawn window stored the displaced center, rebirth preferred it, and the external-sync dirty loop skips handleless entities so nothing ever repaired it). Both now project; the live path's no-WorldRotation fallback reads the body viareadBodyRotationinstead of feeding rapier-2d's scalar angle to quat math raw. After this, every feet↔center conversion in the rapier paths derives the lift from the solver-representable rotation (mantle's own conversions are 3D-only by construction — 2D places always resolve to rapier — where the full quat IS solver-representable).getOrCreateHandle's RigidBodyDesc translation now goes throughprojectWorldVecToRapierlike every later write — rapier-2d'ssetTranslation(x, y)reads y as world Z in 2d-top, and the raw 3-tuple call was handing it the world-Y feet lift (masked until now by initializeBodyState's immediate re-place, but dead wrong on its own).- Deterministic repro tests: 2d-top character + one static box — north vs south walk-in stops at mirror-image feet positions (the ledger 1421 discriminator), body plane position equals written feet under external script-owned movement with a flat WorldRotation re-asserted per tick, no birth displacement, the collider-view ring pinned to the sprite's feet under a flat-lying WorldRotation, and ObjectAPI
setProperty("feetPosition")pinned on both the live-handle and disposed/respawn-window paths. Mantle untouched (3D-only; 2D places always resolve to rapier). - Authored-axes camera contract (ledger 1294 redesign; Slack C09PQJRDF08/1783986300 convergence, jure's spec): custom camera rigs can now DECLARE per-axis view-orientation ownership —
camera.orientation: { source: "look" | "script", authoredAxes?: ("yaw" | "pitch")[] }— replacing every inference of authorial intent from what the script reads or writes. One owner per axis, declared at the API boundary. Script-owned axes are adopted from the authored camera state on every ingest and displayed exactly; mouse input never integrates over them (and the pitch-convention rederive is exempt on an authored pitch — the engine doesn't reinterpret a declared value). Look-owned axes keep display-rate pointer integration, withsensitivitynow a typed field on custom defs. A script→look handoff continues from the displayed pose and drains pending input so buffered motion can't double-count. Fully script-owned rigs skip the renderer-authoritative live path entirely and present timeline-exact — a rig may read look axes without the renderer hijacking the screen (the pre-#9059 death-cam discard class, now unrepresentable by declaration rather than adjudicated by a divergence gate). - UNDECLARED rigs keep exact 5.2.0 semantics, pinned by test: parameters-only steady state on the renderer orientation (config yaw/pitch never adopted after the kind-transition seed, however the numbers move), the mouse-shape classification heuristic unchanged, and the renderer→state angle write-back (
writeCustomCameraAnglesBack) still applied. The write-back is now gated OFF for declared rigs: on the new contract nothing mirrors renderer angles into camera state fields — scripts read feedback throughgetViewAngles(). The declaration dissolves the echo-window/epsilon/headroom/false-adopt machinery of #9059 (already reverted in #9108) instead of re-windowing it. - New read verb
getViewAngles(): { yaw, pitch } | nullin RADIANS on both CameraAPI (canonical view angles — renderer-published for look-integrated axes, authored state angles for script rigs) and ObjectAPI (same three-tier client/server resolution asgetCamera(), which stays degrees).quatToYawPitchpromoted to the shared math library. - The declaration crosses the sim→renderer camera SAB as three flag bits (declared / yaw / pitch —
[]and absent are distinct states), with a writer→reader round-trip test pinning the wire; malformed declarations (bad source, any invalid axis entry) degrade WHOLESALE to the legacy path, and tome-schemas mirrors that exact tolerance jiggle-style (a malformedorientationdegrades to undeclared at parse, never rejects the spec — the patchCamera/DB-fold rollback class). Script→look handoffs drain axis-selectively: the handed-off axis's buffered motion is discarded, a still-look-owned axis's pending input integrates. - Ported the three-era
glowTexturematerial override (albedo-as-emissiveMap) into lume's model lanes — the #7602 three→lume swap never carried it, so every GLB whose glow rodeoverrides.glowTexture: truelost its texture term (the emissive override fell into the FLAT lane because the GLB declares no emissive map). Registry-level port:glowTextureVariantOfmints a cached material twin whose emissive map IS the base color texture, so the resolved override rides the existing map-multiplied lane (writeEmissiveLanes) and the existing full texture tier — final emissive = emissive(tint) × albedo × emissiveIntensity, zero new WGSL. Covered lanes: static batches, characters (whole + per-part subOverrides, rigid children included), and horde batches.glowTexturewas already structural in both batch-signature classes (never uniform-class), so ± glowTexture visuals never share a batch and the emissive binding cannot leak across a signature reuse. - A black (unauthored) source emissiveFactor lifts to white in the glow variant, so
glowTexture: truealone glows the albedo as-authored — the three-era skinned-visual default, now uniform across lanes. - Rigid children of skinned models (a sword on a hand bone) now resolve the emissive TINT into their instance lanes like every other lane writer — the per-character rigid path passed a null override hex (pre-existing), which the glow port made visible as lifted-white × albedo × intensity next to correctly tinted skinned siblings. The fix covers the whole emissive-tint class on rigid children, not just glow.
- glowTexture routing is one shared chokepoint (
glowRoutedMaterial) across the three lane writers. A part that also mounts an override texture (model.materials.<part>texture/pbr) keeps its BASE material: the mount replaces the GLB texture table at bind time and neither mount tier carries an emissive-map slot, so the albedo-as-emissiveMap variant cannot bind — routing onto it would strand the emissive in a lane basecolor mounts never read (darker than not authoring glowTexture at all). The combo warns instead, and the emissive override still renders as a plain glow color. - New log-only diagnostic
model-glow-texture-ignored(engine-diagnostics allowlist) for every surface that still ignores the key instead of doing so silently: a material with no base color texture (nothing to glow), a scripted material (top-level or per-partsubOverridesglowTexture — the script owns shading), a part claimed bymodel.subScripts(the part's script owns its shading), and a part mounting an override texture (no emissive slot to bind the albedo into). The silent ignore is how this regression went unnoticed. - The quality governor's environmental-cap classification is INTERVAL-RELATIVE instead of budget-relative (ledger 1425). On iOS, a never-interacted cross-origin iframe's rendering updates are throttled to ~2Hz (466–660ms walls observed on iPhone 16 / iOS 26.0), and each starved frame drains ~10 sim ticks of catch-up — measured work inflates to ~20ms, which cleared every budget-relative defense: the clean-2× signature never matches 500ms, the throttle-suspect persist gate (#818) refused on busy ≥ 10ms so floor landings persisted and poisoned the device's future sessions, and the interval-capped regime (ledger 1223) refused on its work gate (20.3ms > budget × calm ratio reads "real load, keep cutting"). Three gates now judge work against the arrival interval where no genuine refresh-cadence overload lives: (1) the interval-cap work gate accepts above the calm band once the fused signal passes the deep-cap line (
intervalCapDeepFactor= 4× budget), with the existing unexplained factor carrying the work test there — 20ms explaining a 500ms interval is 4% explained; (2) the throttle-suspect regime's near-idle test widens the same way, so catch-up-inflated busy on a ≥deep-line interval withholds the landing write instead of persisting the floor; (3) the frame-budget guard's environmental gate becomes a full interval gate instead of a 40ms-fallback floor, so a held latch's capped cadence stops re-entering the fused signal and re-descending the ladder the restore just gave back. These sessions now route into the 1223 machinery as designed: restore the pre-descent rung, hold, release when arrivals recover. Everything under the deep-cap line keeps its budget-relative refusals (near-budget real load still classifies as load), work ≈ interval slideshows still descend and persist honestly, and the clean-2× signature path is untouched. The pre-interaction throttle itself (early-interaction gate / same-origin serving, docs/mobile.md) is a separate product decision deliberately not taken here. - Admission stability — killed the one-object-at-a-time pop-in after load (#9287): restored the renderer's apply-ack (markApplied after frame collect, so staleness machinery stops over-firing), a pending-vs-lost delivery guard, immutable resets, and one governed admission gate with a derived work budget and O(tree) admission. Worlds admit content in coherent chunks instead of trickling.
- Places restructure (#9306): runtime-minted place instances (enterPlace createIfMissing, ephemeral/session places) now live in a replicated server-owned registry component and expand to effective PlaceDefs at apply time — they are no longer written into the authored game spec document. The authored spec stays authored; the spec revision no longer moves when a runtime place is minted or torn down.
- God-mode selection fixes: fully invisible (opacity-0) objects rank below visible objects in click resolution — an invisible helper no longer steals the click from the thing you can see (#9452); children attached to model bones refuse selection consistently instead of sometimes selecting and never steering (#9399).
- Field-feed convergence (#9371): a walked-path fade now reaches exactly 0 instead of parking at a faint residue, and small per-frame write progress below the quantization step is no longer erased — slow fades complete instead of stalling.
- FX GPU reservations are sized from the authored population bounds (#9393): long stamped trails and other big authored effect populations stop starving mid-emission.
- The pass-lights fallback tripwire counts completed renders instead of loop iterations (#9376) — slow-booting worlds no longer false-trip into the fallback lighting path.
- Behavior job storage retries are bounded (#9441): a storage write that keeps losing its compare-and-set race re-reads fresh once and then parks loudly, instead of retrying forever.
- New API: spawn.assets.uploadImage (#9357) — frame-callable image upload into servable asset storage, so running games can turn player-made images into real assets.
- New diagnostic: a spec-shape warning when a rotation carries both lookAt and euler (#9387) — euler silently wins, and the engine now says so instead of leaving the conflict invisible.
- Hidden-tab capture timeouts name the real cause in their error text (#9412).
- Play-URL instruments: ?tierOverride forces a device tier (#9416) and ?rungPin holds a quality rung for measurement windows (#9429) — both for test rigs and device farms, no product behavior change.
- Perf rollups carry the viewport's inner dimensions (#9443), making orientation-split performance measurable.
- Savi teaching refreshed alongside the engine: primitives dress from the CDN by default with material-slot mounting taught (#9430), the flip idiom and lookAt/yaw conflation teaching pinned (#9384), game-UI inline handlers window-qualify document-shadowed globals (#9451), full-page screens taught fit-or-scroll on phones (#9438), and workshop wisps checkpoint after each stage so long runs resume instead of restarting (#9456).
- Spawn
lifetimeis now a replicated deadline component (tome/lifetime-deadline, stamped at spawn as spawn tick + lifetime ticks) instead of an in-memory engine timer, and a newtome/lifetime-reapsystem destroys past-deadline entities from whatever world currently simulates them: the owning client, the place host (including a suspension-reverted envelope, which joins the remainder WITH its deadline), or the server — which also reaps the UNHOSTED place remainder, the warm-room case where nobody simulates (ledger 732). Previously the timer lived in the world that executed the spawn — the owning client's sim under client-auth — so a refresh mid-lifetime stranded the server-replicated entity forever in a warm room. Re-arm-on-load falls out free: the deadline rides reset snapshots, re-adoption, and host migration. Only lifetime-bearing spawns carry the component (zero wire delta for everything else);armEngineCleanupTimerstays for world-local duration cleanups (effect/look/highlight clears); thearmLifetimeexec deferred-op dies (the component rides the create row through the merge). Two supporting seam fixes so a simulator's reap actually lands canonically:rail.destroynow authorizes the place host over a suspension-stamped session's adopted envelope (mirroring the StateDeltas drain's suspension rule), and the client-auth upload memo seeds reset-adopted simulated entities so their delete rows drain (an adopted entity destroyed without any intermediate write used to despawn only locally). - Live-mouse-look de-aliasing (ledger 1320, tucker's counter-rotation stutter video): the pointer look path drained an integer movementX/Y Atomics accumulator once per render frame with no time base — device event cadence beating against frame cadence lumped two events into one frame and zero into the next (slow precise orbit-tracking stutter; trackpads worst, 60Hz events on a 120Hz display alternate double/empty frames). The camera SAB now carries a seventh slot accumulating the producer-clock span the look samples actually cover (per-sample gaps, capped at 50ms so an idle pause reads as a fresh gesture), and the renderer drains through
MouseDeltaReader.drainTimeNormalized(frameDtMs): pending motion releases at the velocity it was produced at (frameDt/span per frame) through a displacement-conserving reservoir. Event rate ≥ frame rate ⇒ everything releases immediately (zero added latency for high-rate mice and flicks); added latency is bounded by one producer sample gap, ≤50ms always.drain()keeps take-everything semantics (reservoir included) for every discard/handoff/seed seam, so camera-kind transitions, sim-authority windows, and authored-axis handoffs still leave nothing to double-count. The visibility seam is closed on BOTH sides of the channel: the main thread clears the SAB slots it owns (as before), and the renderer's visibility message — which runs off the message queue even with rAF parked — discards the worker-side reservoir (LiveCamera.discardPendingInput()), so pre-hide motion can never release as one whip on the first resumed frame (adversarial-review receipt: a sparse-cadence 3×300px sweep whipped ~77° of yaw in one frame without it). pointermovelook capture is now coalesced-aware:getCoalescedEvents()sub-samples (feature-detected; Safari and empty-list browsers degrade to the parent event, zeroed-sub browser bugs fall back to the parent delta) feed the camera SAB individually with their own timestamps, so the time-normalization sees the device's honest cadence instead of the rAF-aligned dispatch cadence. The ±350px whip clamp applies per sub-sample; the raw ring's MOUSE_MOVE append stays on the parent event — sim-side gesture traffic is byte-identical. Gamepad look passes its rAF poll timestamp (the main-rAF→worker-rAF phase drift is the same beat) and rides the same de-aliasing; touch look passes no timestamp and keeps its exact raw immediate-release behavior — a sample without a time base carries no coverage, and no wallclock is ever read in the channel.- Feel-layer only, below both camera contracts: declared and undeclared rigs (#9148 authoredAxes) integrate the same de-aliased drain identically (pinned by test); the engine still never substitutes camera framing, and nothing replicated changes. Deterministic beat-frequency regression tests drive synthetic event/frame cadences with no real timers: lumpy pre-fix drain, smooth post-fix drain, conservation, idle-start immediacy, stall-backlog bounds.
- Fixed
api.querysilently dropping positionless entities (tag/state-only data holders spawned with nofeetPosition) on every enumeration path (ledger 1414). The tag-index walker now reads a missingWorldFeetPositionas the pinned origin default{0,0,0}(ledger #359's read contract, matchinggetObject), and the full-scan, tx-overlay (run_script), and spatial paths gained a positionless supplement leg so all paths agree — sensor surfaces (nearest, player scans) read the same origin default. Missing position counts as the origin in radius tests: a query sphere covering the world origin includes data holders, one that can't reach it excludes them. The supplement ridesPositionlessTagBearersResource(maintained at the tag-index chokepoints plus the spatial hooks' position edges, drift-pinned against the component-derived truth), so zero-positionless worlds pay ~nothing and origin-covering queries pay O(data holders), not O(all tagged entities). scripted-material.ts'swalkGraph(param classification + non-finite slot detection) is now explicit-stack iterative instead of recursive — the one residual #9361's review adopted (ledger 1307's recursion half). Content scripts mint graph DEPTH with flat loops (for (…) n = n.add(…)), so the recursive walk threwRangeError: Maximum call stack size exceededout ofcollectReachableNodes, whose catch degraded the build instead of crashing it: every pending auto param forced raw, up toMAX_UNIFORMIZE_ATTEMPTS(4) builder re-runs, and the material lost uniform patchability (every param change cost a full rebuild). Visit order, first-true early exit, fnCall body expansion timing, and the catch-containment semantics around the walk are byte-identical to the recursive form — the walk trades JS stack for heap, nothing else.- Every walk over shade node-graph
inputsis now explicit-stack iterative instead of recursive (ledger 1307's recursion half):StageEmitter.count, theref/expressemission fold (nowref/openFrame/compose/finishRef),validateFnBody's capture check,resampleAtUv's rebuild, and fused-look'srejectNodeclassification +rewriteFrameReadsrebuild. Content scripts mint graph DEPTH with flat loops (for (…) n = n.add(…)— sky-king's 445 generated materials), so the recursive walks turned working authored graphs intoRangeError: Maximum call stack size exceededthrown out of the scripted-material rebuild (izkimar's "Maximum call stack while rebuilding materials" episodes) and into caught two-hop parks for looks. Emission order, hoisting, resource first-encounter order, and node-id assignment are byte-identical to the recursive emitter — the walks trade JS stack for heap, nothing else. Emit-time failures that remain are deterministicShadeTypeErrors, which the per-material catch already parks with a diagnostic and a visible Std/PBR fallback. - Ledger 1395 (Farm Island dig c39c9027): extended the runscript forwarding recipe (
tome/script-forward) to the engine's own rails. Newhook.invokecontrol message (protocol-additive) carries a hook _invocation descriptor — never code; the client resolves the hook from its own compiled spec — sharing the script lane's pending table, ref space, andScriptResponsereply channel, so ref mismatches between the two lanes are unrepresentable. Verdicts are coded (no_client/timeout/interrupted/mode_changed/rejected/hook_error) so fire sites can tell delivery residuals (skips, logged) from the game's own script failures (reported through the same behavior-error rails as local runs, with per-scriptLifecycleHookErrorattribution reconstructed from the wire). - Authority-side executor
tome/hook-forward-authority(server bundle — glued client-side in singleplayer, no-op on the real server): strictly FIFO, one invocation in flight, so a save-at-disconnect can never start before a restore-at-connect settles (the takeover/self-heal clobber order is structurally dead); entries wait (deadline-bounded) for the compiled spec / player entity boot races and answer honestly (timeout,rejectedon revision skew) instead of rotting or guessing. Persistence flows on the existing rails: storage viatome/job-forward(player-scope authorization unchanged), spec mutations viaRoomClientOpcode.SpecMutations. - Room-runtime fire sites gate per fire on
isSingleplayerWorld(mode can flip live): crons forward with per-cron in-flight dedupe (no autosave thundering against a frozen tab);onPlayerConnectedforwards fire-and-track (awaiting would deadlock the join the delivery needs);onPlayerDisconnectedforwards awaited, skipping fast when the client is gone and at shutdown (the pump — and the forward rail with it — is already stopped);onPlaceStartforwards per place in order;onPlaceShutdownis a documented singleplayer skip at teardown.CronScheduler's context is nowrunJob(job, cronId)so the runtime owns the execution-context decision. - Reattach outbox prune and room reset settle queued
hook.invokeframes loudly (interrupted) on the same synthetic-response channelscript.execuses; a networking-mode flip re-routes queued scripts locally (as before) and settles queued hooksmode_changed(the server-context rails run the next fire correctly). - Fix round (adversarial review): three edges hardened. (1) Wake-replay fencing —
hook.invokecarries the server's enqueue stamp (issuedAtMs), compared entry-to-entry on the server's own clockline (never against the client clock): a frozen tab that receives a minute-spaced backlog in one wake burst executes at most the newest invocation (older entries answertimeout, same-cron duplicates are superseded outright even inside the window), and the resident client queue is hard-capped at 32. (2) One-in-flight is unconditional — a started invocation is never swept at its deadline (the deadline is a reply deadline, not an overlap license), so a save can never start while a slow restore still runs; unstarted entries behind a blocked head still expire honestly. (3) Authority-epoch fencing — an invocation reaching a world whose spec says multiplayer answersmode_changedat receipt (the no-spec boot race still queues), and the runtime worker clears the queue on singleplayer exit, so a quick singleplayer→multiplayer→singleplayer flip can never replay a stale invocation. (4) Continuation fence (the common root under the first three) — queue membership alone cannot fence a STARTED invocation, whose closure retains the world/api until settlement: each started invocation now captures a monotonic generation, bumped on queue clear (reset, singleplayer exit) and when the sweep cancels a started cron superseded by a newer resident fire of the same cron, and validated at every resumption point — the ObjectAPI (first touch after cancellation aborts the resumed continuation), the settle path (a stale verdict is discarded; the server's verdict already exists), and the job callback brackets (the fence's abort is recognized and discarded, never reported as the game's job_callback_error) — so a buffered job response or a released await can no longer resume a dead invocation into the current world. Supersession is the only sweep cancellation: a started lifecycle head is never cancelled, however stale — a merely-slow restore stays a passive block (the FIFO ordering claim above is unconditional), because cancelling it would run a later autosave against un-restored state. - Singleplayer mirror job echo-discard (specimen: dump db485cf6, Farm Island — two save-keepers wrote
user/<id>/farm-saveonce a minute: the client authority and a mirror world permanently at spec defaults; last-writer-wins clobbered the real save). In singleplayer the server world still runs behavior scripts (simulation-authorityreturns true on both realms) and drained theirapi.jobsubmissions into the room's real pool with real storage credentials — "read-only" was enforced only for spec mutations (spec-mutation-drain's singleplayer discard).tome/job-dispatchnow applies the same precedent to the job lane: on a singleplayer server world, a builtin server-rails job submitted by a mirror-run behavior is discarded at drain (debug-visible aggregate logtome.job_dispatch.singleplayer_echo_discarded), because the client authority provably runs the same behavior code and its own submission ridestome/job-forwardunder the player'suser/<self>/authorization. The discarded request's callback deliberately never fires (the same silence as any skipped-at-dispatch request) — a synthetic verdict would feed the mirror's scripts a result the real job never produced. - Exemptions, by provenance: the lifecycle/cron lane (
TomeJobRequest.lifecycleSubmitted, stamped atapi.job()whenLifecycleContextResourceis active) is not an echo — hooks and crons execute only on the server world today (ledger 1395; PR #9366 re-homes those invocations to the client authority, whose jobs ride the client-mode dispatch + job-forward rail and never enter the server world's queue, so the two fixes compose); relocated-interaction continuations (this machine holds the hook's only copy); and custom spec jobs, which stay mode-both local by design — the builtin server rails were the write-capable hole. - Sprite apex, following #8684's text apex. Motion vectors now ride depth: on TAAU frames the depth-writing cutout sprite class stamps honest per-instance motion vectors (
lume_velocity_pairover VS-computed now/prev world poses) instead of leaving the velocity MRT masked — the fix for walking-character TAA ghosting, where the resolve's closest-depth tap reprojected a depth-writing sprite with the background's velocity. Entity motion comes from a new per-instance prev block (instances.ts, 28 → 36 floats) retained by the per-frame pack; camera-dependent billboard bases resolve in the vertex shader against the newframe.prevViewuniform, so camera motion stays repack-free and honest. History reseeds (zero object velocity) on first pack, cull gaps, re-homes, dimension flips, and pose-basis draw replacements (billboard/parallax/repeatX/anchor/size-presence — the retained pose was presented under the old basis and differencing across it would fabricate velocity), with ONE shared same-key adopter serving both thedraw/spriteanddraw/sprite-instanceswriters. The whole-tile repeatX camera recenter applies to both poses (a presentation re-parameterization, never differenced). Transparent sprites keep the writeMask-0 velocity doctrine. - Composes with #9314's restored pixel-inferred 3D cutout default: the walking-character population — a prod sweep found ~2,500 pixel-classed sprites, including 56/59 3D player sprites — rides the depth-writing class this fix lives on, so pixel characters get honest motion vectors with zero authoring. Known residual (2D parity, pre-existing): authored
opacitydoes not enter the cutout discard, so a pixel-classed sprite faded by opacity keeps writing depth (and now velocity). - No sprite distance fade — deliberately (evaluated and dropped in review round 3): in 3D the transparent class is the DEFAULT sprite class (~3.9k live sprites across 216 games in the prod sweep), sprites have no core/fringe twin to fade honestly, and camera-attached sprites present at constant pixel size, so text's 3→6 px band would have permanently dimmed real content. Both sprite classes keep authored alpha out to the pre-existing hard distance cull.
- No creator-facing spec changes; sprite billboarding and 2D sort-band behavior are unchanged.
- Staging/dev-only debug-dump gesture (jure, 2026-07-16): five consecutive taps within 2s inside the lower-left 80×80px corner of the game view post
spawn:debug-dump-gestureto the parent frame; the kiln page hosting the iframe answers by running the same debug-dump capture the studio Debug chip runs and copying the shareable dump link (touch devices have no F2/Copy path into the perf tooling). New moduleengine/client/debug-dump-gesture.ts— a pure corner-tap tracker (a tap qualifies only when its pointerup lands within 250ms of its pointerdown with ≤10px of travel — the movement stick's spawn region contains this corner, so stick grabs/flicks never advance the streak; out-of-zone or non-tap presses reset the streak; the window prunes stale taps) plus a hostname gate:staging.play.bigspawn.net,*.staging.bigspawn.netlegacy aliases, and the localhost family install the listener; prod (play.bigspawn.net) returns null from the installer and the gesture structurally does not exist there. - Input honesty: the listeners observe pointerdown/pointerup/pointercancel capture-phase and passive — never preventDefault/stopPropagation — so every touch (including the five taps completing the pattern) still reaches the game.
- Wired in worker-browser-host beside the F2 inspector shortcut installer, cleaned up at host dispose.
- One truth for placement (ledger 1428, the towerwood class-kill): creator-facing terrain height/surface reads now answer from the AUTHORITATIVE sampler —
sampleReplicatedTerrainHeight(pure generator eval + replicatedterrain:heightfield layers + voxel edits), the same samplery: { terrain: N }pins resolve through (position-utils) — never from loaded chunk outputs. Flipped surfaces:api.getTerrainHeight,api.getTerrainNormal(object-api.ts), and run_script's baregetTerrainHeight/getTerrainoverlay (exec/engine.ts). Previously these defaulted tosampleTerrainHeight, which prefers whatever chunk output is resident at whatever LOD the device streams (alignment.tssampleLoadedTerrainHeight) — when the quality governor stepped a device's terrain streaming profile down (streaming.ts terrain-reach seam), hilltops shaved ~5.5m in the loaded picture and scripts planted objects at the blurry LOD surface while spec pins stayed put (towerwood, staging app e09e686e). - The live sampler stays engine-internal, verified caller-by-caller: chunk rescue + the collider parity probe (terrain-systems-shared.ts), collider gating (collider-gate.ts), server room placeholders (room-placeholder-manager.ts), the god-mode visual overlays (selection-outline.ts, scatter-footprint.ts — drawn onto the rendered mesh), and
cameraApi.getTerrainHeight/getTerrainNormal(camera-api.ts — camera scripts relate to the streamed ground on this client; doc comments now say so). NPC steering/nav never used it (nav-grid samplesheightFromDefinitiondirectly).sampleTerrainSurface's explicitsource: "live" | "replicated"option is unchanged — the flip is at the creator-facing call sites. - Consistency dividend: god-mode drag/placement lanes (default-editors, placement-flow, brush session, authoring-api) compute terrain-relative offsets via
api.getTerrainHeightand bakey: { terrain: N }pins — offset math and pin resolution now read the same surface, so a drag on a stepped-down client no longer bakes an offset wrong by the LOD delta. The exec overlay lane was already answering replicated by accident (the transaction-overlay proxy's identity misses the output store's world-keyed WeakMap); it is now the explicit contract. - Semantics note, stated honestly: scripts that RELIED on reading the render-LOD/loaded picture (if any) now see the true ground — including edit-free voxel places, where
getTerrainHeightpreviously preferred loaded chunk samples; mid-cell heightmap reads, which now eval the generator exactly instead of bilinear-interpolating baked samples; and legacy UNTAGGED height-sculpt journal edits on old heightmap worlds (chunk builds bake them, the replicated lane does not — pins never saw them either, so reads still match pins exactly; they diverge from the rendered/collider ground on those old worlds, pre-existing pin-lane behavior now shared by reads). Red-first coverage in terrain-height-one-truth.test.ts: synthetic coarse-LOD-resident chunk (5.5m shave) — api.getTerrainHeight/getTerrainNormal/run_script answer generator truth, the internal live sampler still sees the loaded picture, and the API read equals what a terrain pin resolves against. - Rapier corruption quarantine is now structural instead of flavor-matched (ledger 1442, dig 06a668b1): any exception escaping the wasm step itself (
world.step) marks the runtime corrupted unconditionally — a stack-overflow RangeError unwinding through wasm frames poisons wasm-bindgen's borrow flags exactly like a Rust panic, but the old matcher rethrew it, leaving the world poisoned-but-unmarked and every physics-touching system throwing "recursive use of an object … unsafe aliasing" per tick until page reload (prod app 8b8a0b7d "Canyon Strike", two sessions, ~3.5 min and ~28 min of zombie physics). - The per-tick rapier funnels that entry-check
corruptedbut could never SET it —syncWorldFromRapier,syncExternalTransformChanges,cleanupRemovedPhysicsEntities— now quarantine on any escaping throw and return, so a poisoning that first surfaces there self-quiets next tick andensureRuntimeForPlacerebuilds a fresh world. isRapierCorruptionErroradditionally recognizes stack-overflow messages ("Maximum call stack size exceeded", Firefox's "too much recursion") for the remaining pattern-gated call sites (character controller, trigger overlap, sensor broadphase, warmup). Over-marking worst case is one cheap world rebuild + ECS resync — the rebuild path was already proven in prod. Both rapier lanes are covered (the runtime is shared across 3d/2d-side/2d-top).- BREAKING (server internals): the four server-side asset-metadata fetch rails (
appearance/bounds-prefetch,appearance/socket-prefetch,appearance/parts-prefetch,sprite/metadata-hydrationserver halves) are deleted from the multiplayer server world. Derivation re-homes to the place host's browser (client-bundle systems gated on the live place-host claim; the singleplayer authority keeps local landing), which uploads over the newrail.assetMetadatacommand — host-gated, budgeted (16/tick, 64KiB/command, ≤64 sockets/parts per model), validated relay-side with the same predicates the fetch rails ran, idempotent (re-sends of landed truth drop silently), reference-checked (only assets a live entity or the spec names). The relay applies through the shared spec writers (tome/asset-metadata-writes) — three-way landing + one TomeSpec revision bump per accepted command. - Additive spec vocabulary:
assets.metadata[textureId].sprite2d = { version, meta | null }(canonical-texture-id keyed;meta: nullis the replicated terminal "no 2d metadata" answer). Sprite size/anchor/collider upgrades are now pure realm-local computations over the replicated entry. - The sockets/parts warm/poll split is collapsed: one authenticated host-session fetch both triggers the MagicCDN cook and reads the sidecar (cooking-base refunds preserved: 502/504/429 × 40).
- Terminal-refusal semantics for the surviving server asset fetcher (
physics/collider-assets/server, plan §(g)): a pre-I/O egress-allowlist refusal (classified by the seam's stable message contract, drift-pinned againstresolveAssetFetchTarget's real throws) or a dead 4xx (isDeadAssetHttpStatus, read structurally viaresponse.statusincl. onecausehop) parks the cook request for the room's lifetime — one runtime log + deduped DM — while 202/5xx/verdict-window classes keep their existing ladders.gltf-hull-extractornon-ok answers now throwGltfFetchHttpError(structural status). - Fixed: multi-hop asset I/O chains inside the engine isolate (the collider rail's probe → GLTF → .bin sequence) were canceled by workerd after the first hop settled — colliders silently stalled as placeholder boxes on the DO tier. Chain owners now register their end-to-end promise (
adoptAssetIoChain, wired to the floating-I/O registry by the isolate entry); the workerd asset-rail truth test drives the full three-hop chain. - The DO relay stops fetching models for metadata: the slim-server manifest's asset-authority group is down to
physics/collider-assets/server+tome/authored-clip-fold(which fetches nothing — it is a patchAssets writer). - Terrain decoration birth (the 0.25s grow-in,
DECOR_TRANSITION_SECONDS) is now once-per-content instead of once-per-GPU-arena, killing the first-load whole-field pulse (jacob's report: "decorations grow and then shrink and then grow a few times"). Three re-arm paths died: (1) entry-arena rebuilds (ensureDecorationEntriesondecorationPoolsKey/config-revision/device changes) now carry eachlayerId:itemIndex's consumed birth — and its last known draw lane — through aDecorationBirthCarryledger instead of mintingbirthPending: true, so every first-load lod-pool arrival no longer re-grew the whole field from zero; (2) draw-readiness re-arms birth only on the null → non-null edge ofdrawReadyKey, never on value changes — a fixed→scripted material compile landing, a velocity-MRT flip, or a texture-lease upgrade keeps placements and grown scale (the texture-adoption re-arm inbindEntryTexturedied with it; a FIRST texture arrival is itself the null → non-null edge); (3) a previously-occupied tile-map cell keeps its birth when chunk refinement re-allocates its pool SLOT (tileMapCellBirthno longer compares slot numbers), so streaming-era slot churn stops re-birthing chunk tiles. Genuinely new content still plays the grow-in: never-revealed items mintbirthPending, previously-empty tile cells stamp fresh reveals, and content returning after a real drawability gap (or a teardown-shaped clear) re-births. Regression-pinned indecoration-firstload-pulse.test.ts(all four cases red on the pre-fix engine); steady-state quiescence (F15) untouched. - Script timers (
api.runInSeconds/api.runInTicks) now survive simulation-authority handoffs in the world that armed them (the timer half of ledger #732's lifetime-deadline work). Timer dispatch used to CONSUME a due timer whose owner another world currently simulates (state.timers.deletebefore the simulator skip), so any authority round-trip — an alt-tab suspension takeover that reverts, a host seat migrating away and back, a disconnect grace reverting an envelope to the server — silently killed every pending timer for good. Dispatch now retains such timers (the newisOwnerSimulatedElsewheregate: exactly the two simulation-authority clauses ofshouldSkipBehaviorEntity, without its liveness/fault clauses, which still consume) and fires them on the arming world's first simulated-here tick at or after due — the same "expired deadline reaps on the first simulated tick after the stamp" semantic astome/lifetime-reap. A retained timer still dies with its entity, withcancelTimer/script-edit reaping, or with its world: a departed player's timers die with their tab — the callback is a closure, and code never rides the wire. That boundary is now documented onrunInSeconds(generated API docs + skill reference): must-survive deadlines belong in spawnlifetime(replicated, #9051) or in state checked fromupdate(). - Hierarchy solve is O(changes), not O(objects), per tick (#9593, ledger 1463 wall 1): steady-state ticks expand the hook-fed dirty set downward through a cached inverted
TomeParentadjacency and visit only that expansion in cached topo order, instead of walking the full sorted entity list to find the dirty few. Zero per-tick allocation; boot andrequestHierarchySolveAllpasses still walk everything. DEADZONE-shaped bench (7,245 parented entities, 36-entity per-tick dirty set): 1.19ms → 0.058ms per tick — the prod specimen was paying 22–65ms of hierarchy solve EVERY tick, alone ≈ the whole 30Hz budget. - Spec hash is per-node merkle with identity memos (#9595, ledger 1463 wall 2): the once-per-writing-tick
TomeSpec.hashstamp hashes structurally — leaves from canonical scalar forms, containers from child hashes, memoized in an identity-keyed WeakMap — so hashing a structural-sharing successor costs O(changed path): 162ms → 0.5ms median per writing tick on an 8.4MB spec. The client's hand-synced stringify clone dies (both realms import the ONE shared module), and the two sanctioned in-place live-doc writers invalidate memos path-precisely. - Spec mutation fold is copy-on-write with delta-confined signature invalidation (#9596, ledger 1463 wall 4):
foldSpecMutationsstopsstructuredClone-ing the whole doc per forwarded mutation batch — every mutation site owns exactly the path it writes through, untouched subtrees keep the base doc's identity, the no-op gate compares structurally with identity short-circuits, and the TomeSpec stamp goes lazy. An object-array-confined write evicts only the touched rows' diff signatures (plus their expansion-derived ids) instead of dropping all 7,263; anything unattributable falls back to the whole-map drop. Bench at DEADZONE scale: fold 45.3ms → 0.23ms, fold lane end-to-end 151.5ms → 2.7ms with zero whole-spec hashes. feetPosition: { attach: "camera" }now resolves in the mesh, primitive, and model render lanes (#9598, ledger 1468 — finishing the documented primitive only sprites honored): attached roots re-present each frame against the live frame camera (post-smoother, zero-lag by construction — script-driven re-posing swims ≥1 tick behind and cannot be fixed script-side), subtrees transport uniformly in the root's camera-local space (bone-attached children included), depth clamps to live near/far, physics anywhere in an attached subtree refuses loudly at the write boundary, andquery()filters subtree members exactly like their root.- The model render lane honors
material: { depthTest: false }(#9607, ledger 1472 — the documented flag every other lane already read): overlay-pool semantics mirrored exactly from primitives (depthCompare always + no depth write, forced transparent class, draws in the post-alpha overlay slot, never casts shadows), across static batches (LOD + instances included), hordes, and characters with per-partsubOverridesand live material-write re-resolve. Scripted materials already honored the flag; plain GLB model materials silently ignored it. - camera-first-person skill teaches the held-item viewmodel (#9604, widened to GLB models in #9610 once #9607 landed): spawn with
feetPosition: { attach: "camera", x, y, depth },realm: "client"+audience: "local"for per-viewer items,material: { depthTest: false }for the no-clip lane (primitives, mesh parts, and GLB models all honor it), ADS = offset easing +setCamera({ fov }), bob/sway/recoil = per-tick offset math. The section opens with WHY scripts can't do the transport themselves — the fact that retires the whole re-posing class two power creators hit in 3 days. - places skill: state that must survive a place transition rides the
enterPlacecall itself — seed viacreateIfMissing.objectsor pass the instance id you hold — never a storage write you re-read on the other side of the boundary (#9602, the Diceweaver three-patches-in-four-days class). - Interact prompts hide when their source is hidden for the viewer (#9608, ledger 1473): a display-honesty gate reads replicated client state — source not drawn (the renderer's own
override ?? basevisibility semantics), source riding the viewer (local player'sTomeParentattachment subtree or a camera-attach subtree), source destroyed — while prompts on items held by OTHER players keep showing (steal/trade designs stay honest). Kills the permanent "Take" prompt floating on your own held gun; the distance gate is untouched. normalWorldis available in the vertex stage of lume scripted-material assembly (#8768, jure's shade-vocab-gaps class from the 5.2.0 mint): vertex graphs read the exact normal the fragment varying is fed (lume_normalMatrix3cofactor transform on instanced lanes — correct under non-uniform scale — world-premultiplied skinned pose normals, raw attribute on"none"), hoisted ahead of thepositionNodeblock, so displace-along-the-surface-normal compiles instead of parking the material to its white Std/PBR fallback. A normal-less layout parks with the honest missing-attribute error.pushLookof a scripted look carryingfade > 0or partialstrengthnow teaches loudly (#9594, ledger 1465 honest floor): scripted looks are selected by priority alone (the strength ramp sweeps only built-in recipe contributions), so those options hard-snap — the authority world emits one content-deduped runtime-log warn + DM (scripted-look-fade-strength-ignored) naming the script self-mixing recipe (actx.param("fadeMix", 1)swept by the pushing script). Push behavior itself is unchanged; honored shapes stay silent.- Script-edit swaps stop freezing the room (#9572, SS-B of the swap-freeze program p-d2720022): the replacement isolate boots IN PARALLEL while the old isolate keeps serving (mid-boot ticks lose nothing — the snapshot hasn't been taken yet), the handoff runs snapshot → restore → inline apply pre-flip, and flip → durable ledger append → reattach is one sync-contiguous block. The measured 331–468ms of per-swap egress silence collapses to the snapshot encode; any failure — boot, restore, apply throw, or a structured apply rejection — leaves the OLD isolate the room, unswapped and coherent.
- The singleplayer run_script/hook forward lane is honest end to end (plan p-d2720022 incs 1–2): a no-client SP room reroutes
run_scriptinto the server world's script queue with full transactional semantics instead of fast-failing (#9562); device visibility becomes its own wire fact beside projection — initial state on the socket attachment, transitions on a new opcode (#9579); forwards carry an ack fence, so a hidden/frozen tab's offer voids in ~2s with a fact verdict +script.revokeinstead of a 20s "most likely backgrounded" guess — 743 of 999 weekly run_script failures were that class (#9581); and a voided forward RETARGETS to the server exec lane, so the change executes instead of merely failing fast (#9586). - Join-honesty deadline on the DO boot lane (#9580, ledger 1460): an accepted client socket must reach its welcome within a deadline or close loudly — the forever-open zero-byte join hold (the staging UI-only-spec wedge; a boot-ladder await that hangs without rejecting, the one shape no detector covered) becomes a detected, telemetered close instead of a silent black screen. Ships the
sweepSocketsone-door discipline the sibling DO landings build on. - Audio rate-limit honesty covers the payload-fetch face (#9573, pc-152aff3c): a 429 landing on the real asset fetch after the probe passed now parks the clip with the same typed quota verdict as probe 429s — no attempt burned, one scheduled re-attempt when the window lifts — instead of burning to terminal
failedClipsin seconds; the retry window reads from the 429 body when theRetry-Afterheader is stripped; a windowless 429 takes one bounded 15-min fallback re-attempt, never a tight loop. Theaudio-quota-parkeddiagnostic + DM now carry the exact capped URL. - Scripted-primitive re-mints merge runtime-diverged params on non-replace applies (#9533, ledger 1454): a script save re-minting a scripted primitive composes new script content × merged params — doc-changed keys win, runtime-diverged keys survive, the
mergeRuntimeStateWithSpecDefaultsthree-way law applied toprimitive.params— and both re-mint call sites hand the SAME effective props to appearance and physics, so colliders rebuild from exactly the params the visible mesh was minted from. Replace applies (revert_to_version) still fully reset, matching state's #296 semantics. Kills the carve-ledger class: visually-pristine hills whose collision remembered every crater. - Content-policy asset rejections reach Savi (#9528, ledger 1453 — the engine half of #9495):
getUnreadySceneAssets()grows a terminal"rejected"status (+ provider category) rendered FIRST and deliberately without an elapsed-time suffix, and the first cached-verdict 400 fires themodel-load-failedteaching diagnostic immediately with a terminal park (zero further network) — instead of riding a 5-attempt retry ladder whose eventual generic message gave the wrong advice and whose oversized key was silently dropped for exactly the long-prompt ids content policy rejects. - Savi game-alert flood mechanisms (#9523, jacob's "absurd amount of game alerts"): behavior-error DMs re-key on the unit Savi fixes — (hook, scriptRef) at session scale, re-armed by her own spec save, never by the clock or the volatile error message;
[client-reported *]teach rails dedupe spec-version-scoped; idle drips coalesce. DM cadence only — the getLogs stream keeps every signal. - The missing-target call-ignored LOG path is wall-clock once-then-count (#9529, ledger 1225): the first ignored call per (operation, target id) logs the full teach dump once per room lifetime, identical repeats fold into one compact counter note per 60s, and post-escalation summaries are time-gated per signature — a hot loop that put 35,711 rows into the kernel error lane in 2h now worst-cases ~1 line/min. run_script's exec overlay still returns every warning verbatim.
- Lume sprite-material honesty (#9506, the Diceweaver tab-away sprite vanish): failed pipeline builds retry bounded (3 attempts per budget, backoff, re-kicked on access) instead of parking permanently; texture loads carry tick-clock deadlines that abort wedged transports and hand the episode to the ordinary retry ladder; and a residency sweep (
retryParked+ deadline revalidation) runs on visibility hidden→visible and on projection-reset baselines. Terminal parks (tombstones, dead 4xxs) hold — re-probing them would re-open the tombstone-hammering class. - Rapier quarantine classifies the whole wasm RuntimeError family and stops blaming scripts (#9502, ledger 1451 — extending #9471's structural quarantine): any live
WebAssembly.RuntimeErroris corruption structurally (message families kept for cross-realm copies, now case-insensitive), and post-quarantine hook errors reframe to physics-wasm-corruption attribution across the ops rail, behavior-error reporting (one storm = one stable dedupe key = one DM), and the client fault wire — the specimen session rained 9 wasm faults as behavior-hook errors on an innocent script and Savi built a wrong narrative for the creator off exactly that mis-attribution. Quarantine is surfaced to Savi and the player instead of physics silently going dead. - Tint-without-albedo is the authored flat-color state for heightmap terrain materials (#9481, ledger 1446):
albedois optional whentintis present —buildMaterialPackemits the internal color layer as a first-class authored state instead of a warned fallback demotion,patchTerrain/schemas accept tint-only (albedo: nullconverts), deleting an inert albedo rides the shading-only fast path (zero visual change, no rebuild), and the atlas warning keeps firing only where a DECLARED albedo can't pack — a real defect signal. Kills the eternal-warning loop on intentionally flat painted ground (~92k-token hidden Savi turns). - Place-host assignment gains adoption-confirmed tenure (#9394, p-4248883b): every assignment is provisional until the host proves adoption via a one-shot empty host-stamped delta through the normal outbox; unconfirmed-past-timeout re-elects preferring a different Streaming candidate (a sole candidate holds — no epoch churn on a timer), a shared expiry/re-seat cooldown prevents ping-pong with the 1257 forced-re-seat rail, and revocation beats expiry by rule. A join-window host that never received one snapshot can no longer hold an occupied place while a fully-Streaming joiner waits.
- DO-tier ops hardening from the 07-17 staging storm: wedge recovery gets a poison ladder — 3 consecutive terminal escalations on unchanged spec content identity inside 15 minutes parks the room cold instead of boot-looping the same poisoned spec forever (park-without-abort: workerd's output gate provably discards same-task writes, so the strike lands durably and disposal is in-process) (#9574);
POST /rooms/{id}/resetis the DO-tier operator kill switch, returning a poisoned room to never-booted cold with a structural reset latch so mid-flight continuations can't re-poison it (#9577); and the shell memoizes asset-fetch allowlist refusals to one loud log row per (room, URL) with byte-identical rethrows, so frozen old-pin retry ladders stop flooding the error lane (#9560). - Internal/observability: per-pass GPU timestamps ride the perf rollup + farm collection (#9597); SS-A swap-freeze phase split + tick-liveness instrumentation (#9565) and run_script emitter-lane counters (#9563); RoomHostLoopback RPC rejections un-blinded in the fleet logpush stream (#9517); MP visibility-aware host demotion pinned e2e (#9561); the do-sim bench's staging-floor leg tears down its room (#9576); workerd-exec settle flake fixed with signal-bound waits (#9519); an ECS delta recorder in the F3 ECS tab (#6918); DeployDefaultWorldStaging targets the static RoomDO host flavor (#9613) and stage.yml deploys the default-world-staging worker on master pushes (#9590).
- Depth-sorted sprites stay instanced (plan p-ce2385b6).
sprite.ySortno longer nulls the batch key for depth-writing sprites (sprite-logic.ts getSpriteBatchKey) — every ySort sprite used to become a STANDALONE batch (own storage buffer, per-frame CPU sync, full pack), and dense top-down games stacked hundreds (overgrowth: ~371 single-instance batches/frame → 9.74 ms lume/render CPU on an L4 rig; prod F2 showed the same shape). ySort sprites now share theirtexture|blend|layer|order|depthMode|lit|filterbatch: the per-frame full pack orders a 2D batch's instances by the standalone era's exact rank (computeSpriteSortKey, stable sort, view-depth tie-break — equal-y never flickers), and each instance carries its own sort band depth in a former pad float (instances.ts sortDepth,flagsB.z), so the vertex shader's clip-Z override is per-instance and the depth test keeps interleaving depth-sorted sprites ACROSS batch boundaries exactly like the standalone era. Cross-batch draw order stays the key's layer/order band (ySort only ever ordered within its layer/order neighborhood — zQuant clamps inside half an order stride). 3D never rank-sorts (zQuant is 0 in 3d): billboards keep physical depth and the existing back-to-front painter order; ySort in 3D is now fully inert instead of an accidental unbatcher. Scope: the batched path covers depth-WRITING sprites — cutout, the 2D alpha default and the entire dense-dressing class. A 2D ySort sprite that resolves depthMode "transparent" (authoredcutout: false/alphaCutoff: 0, or a light-like blend) writes no depth — its cross-batch order is pure blend order, which only the per-sprite alpha-bucket sort preserves — so it keeps the standalone path (the pre-batching behavior, carved out ingetSpriteBatchKey; pinned by the sprites-ysort fixture's soft column + store/key tests). receiveShadow/parallax/repeatX/camera-attach standalones are untouched. - Fixture receipt (371 packed sprites, 358 ySort over 12 textures + 13 props, identical instances both arms): 361 batches → 15, 361 alpha draws → 15, 722 steady-state writeBufferCalls/frame → 15. Real-GPU ordering parity pinned by the new
sprites-ysortrender-harness fixture (2d-top, two textures, opposite pairwise overlap winners — impossible under whole-batch depth; negative control verified to fail) plussprites/text-2d/emissive-2d/scripted-sprite-churngreen on the L4. The A/B ceiling this chases: stripping the 358 standalone sprites measured lume/render 9.74 → 2.74 ms (−72%), draws 154 → 43, writeBufferCalls 288 → 66. - The sprite material staging compare froze:
packBatchcompared the f64 band depth against its Float32Array staging, which never matched, so EVERY 2D sprite batch rewrote its material uniform every frame. The staged value is nowMath.frounded; a static 2D batch settles to one instance-buffer write per frame and zero material writes. (The named non-scope follow-up — revision-gated packing so shared buffers stop full-repacking every frame, model-batch.ts:483 precedent — is deliberately NOT in this change.) - Never-acked client-auth creates retire instead of leaking forever (#9675, ledger 1493 — jesse's peakaboom fx exhaustion): a client-auth entity CREATE rides the fire-and-forget StateDeltas lane, and a flood-dropped / outbox-pruned / chain-unwound create left a permanent local-only ghost whose spawned fx (windowless looping populations) held fx-arena population rows until the 256-row table packed solid and every new effect refused. The server drain now acks every create row it ADMITS (
create.ackcontrol on the reliable outbox); the client arms a 3s grace per published create (cleared by the ack, by the row appearing in room projection, or by projection reset), and an expired-unacked entity despawns throughdestroyEntityWithHookwith destroy-preview semantics — the cascade takes its spawn-owned subtree (the fx it spawned die with it), frees the arena populations, and publishes delete rows so a late-applied create converges to dead instead of forking. One creator-language runtime-log line per retire. A version-skew latch arms the retire judgment only after the room demonstratescreate.acksupport, so a new client on an old room can never mass-retire healthy creates. Every arena population's backing entity is now room-admitted, local-realm juice, or retired within ~3s — there is no fourth state. - The last permanent-absence collider lane with no terminator is closed (#9656, ledger 1491): a collider cook that succeeds (200 OK, valid GLB) but contains zero static geometry — every mesh skinned, or an empty cook — used to ride the flat 5s retry ladder forever (~12 req/min/asset; the live specimen held magic-cdn at ~59 req/min and 429'd the app's other assets for the room's lifetime).
EmptyColliderGeometryErroris a typed error at the single empty-hulls throw site, classified structurally (instanceof, one cause hop — never message sniffing), latched into the existing terminal machinery: one honest teaching line to getLogs + deduped DM ("this asset has no collider"), zero further fetches for the room's lifetime. Per-requestKey, so a manifest-signature change mints a fresh honest attempt. Fires only at the outer catch after both extractor lanes settle (the #9094 zoo-statics lesson). - The correction blend (
correction-blend.ts, projective velocity blending for authority handoffs on lease-class dynamic bodies) projected the abandoned trajectory's POSITION ballistically (fromPos + fromVelPerTick·dt) but held its ROTATION as a frozen quaternion nlerped toward the live stream — every seeded blend froze a rolling body's drawn spin, then swept it, so the roll twitched per correction and died at rest (ledger #1481, george's twitching cube). The blend state now carriesfromAngVelPerTick, derived from the ring's last snapshot pair exactly likefromVelPerTick(angularVelocityPerTick: shortest-arc delta quaternion → rotation vector per tick — no wire dependency), and the abandoned rotation is projected (q(dt) = exp(ω·dt/2) ⊗ fromRot) before the shortest-arc nlerp. Zero angular velocity reproduces the previous behavior exactly; the module stays pure and mode-independent. squash()'s [0,1] clamp onamountat the ObjectAPI boundary (in place since #4606, verified against current master) was silent —squash(1.4)quietly becamesquash(1)and neither Savi nor the creator ever learned the input was out of range. Out-of-range amounts now warn through the mutation-warn rail (getLogs-visible), and the boundary behavior is pinned red-first: above 1 squashes fully, negative stays a no-op, in-range passes through untouched.- The DO-tier behavior-compile refusal class is dead, end to end (the 07-16→07-18 staging arc): workerd only permits codegen at isolate startup, so scripts entering a live room outside the /update lane could never compile until an isolate swap — and the swap machinery had no reliable trigger and then no safe concurrency. Four merges close it: RoomDO self-pokes its own update lane on script-bearing drains, so an exec-authored
setScriptconverges to a compiling isolate without waiting for a foreign write (#9643); adoption honesty — the scripts-hash is never adopted over compile refusals, mid-hydration specs no longer downgrade the swap to an in-place apply, and a codegen refusal forces a mandatory heal swap, converging in ≤1 swap (#9652, ledger 1488); exec-authored script edits arm the room's own bounded-backoff heal poke directly (#9661); and the heal is observable — a successful apply retracts the error ring's inert-script report instead of leaving a stale refusal row for Savi to chase (#9673). - Script-edit swaps have exactly one arbiter (#9689, the 07-18 staging isolate-restart storm): one save used to dispatch the update lane from three independent triggers (kiln fan-out, exec-response self-poke, drain self-poke) into mandatory-swap machinery — overlapping replacement boots tore every connected client (reconnect races, iframe remounts, ~16s world reboots). Now one in-flight swap per room keyed by target scripts-hash (same-hash triggers absorb into the in-flight swap, different targets serialize), both self-poke lanes are check-then-dispatch (already serving the fetched hash ⇒ no dispatch), the blind "unknown live hash ⇒ swap" default is dead (swap only on a KNOWN differing hash; unknown live hashes apply in place and heal on positive refusal evidence), and every pre-flip swap throw logs a
script_edit_swap_failedreceipt — the old isolate provably keeps serving. Make-before-break preserved: one save, one replacement boot, sockets stay open. - Cursor-need UI is now EDGE-triggered instead of level-triggered (pointer-lock-manager.ts): a visible
[data-modal]/[data-needs-cursor]element frees the cursor when it APPEARS, exactly as before — but a pass-through canvas click (one that reaches the canvas rather than any UI pointer target) landing OUTSIDE the bounds of every visible cursor-need element acknowledges the visible set and re-engages pointer lock in the same gesture. Acknowledged-but-still-visible elements no longer hold mouse-look hostage; a fresh appearance (new node, hidden→visible again, attribute re-added) is unacknowledged by construction and frees the cursor again. - Bounds-aware click gate: a pass-through click INSIDE any visible cursor-need element's rect is swallowed exactly as before (no acknowledge, no relock). This protects the common prod idiom — a
fixed inset-0; pointer-events:nonewrapper carrying the attribute around an interactive card — wholesale: its bounds are the viewport, so no misclick beside the card can relock over a mandatory-pick menu. Interactive full-backdrop scrims were already safe (their clicks never reach the canvas). Small ambient panels gain the click-to-relock rescue. One narrow corner trades a click: after a user-Esc unlock, a dead-zone click inside a wrapper's bounds is swallowed WITHOUT re-arming the relock gesture, so a panel that then closes withoutrelockPointer()leaves the player one prompted click from lock where prod today relocked silently — accepted so that a click in a modal's screen region can never pre-authorize a later silent relock. data-modaljoinsAUTOMATIC_CURSOR_UNLOCK_SELECTORSas the taught vocabulary;data-needs-cursoris compat-accepted with identical mechanics plus a one-per-element deprecation nudge ([Tome]console line +cursor-attribute-deprecatedengine diagnostic → getLogs, log-only).api.on()hitting the transactional guard inside an onSpawn that is itself running transactionally (the entity was spawned from run_script) now reports a hook-context message — register the handler fromupdate()on its first run (guarded with a once-flag, sinceapi.on()adds a new subscription every call) or spawn the entity from the spec — instead of the body-context "create a script with api.on(...) instead", which pointed authors at exactly what they had already done (papercut pc-21d6a350: the handler registration is permanently lost post-merge because onSpawn is never re-run live-side).failTransactionalGuardaccepts an optional hook-context variant; body-context messages and the sticky exec-fault semantics are unchanged.- Cursor-need acknowledgment now survives same-id element replacement (pointer-lock-manager.ts): when the 500ms scan finds an acknowledged element gone from the visible set and an unacknowledged visible cursor-need element carries the same non-empty
id(first in document order, if the id is duplicated), the acknowledgment transfers to the replacement instead of it reading as a fresh appearance. Savi-authored UIs re-render through a morphdom-style DOM diff that can replace a modal root outright (new Element identity, same id in the markup), which previously re-freed the cursor on every render of an already-clicked-past panel — flicker plus intermittent re-capture on any UI that re-renders each second. The bridge rides the existing absence grace (the same 2-tick eviction window): an aged-out entry bridges nothing, an empty or missing id never bridges, and a fresh never-acknowledged id still frees the cursor exactly as before. Accepted trade, pinned in tests: a genuinely new modal that recycles a just-acknowledged id within the grace window inherits the acknowledgment. Teaching unchanged: a stableidon the modal root remains the recommended authoring pattern. - Look author-colors render display-honest (#9091, ledger 1284 — jacob's waystead washout): the look pass mixed author-supplied color targets raw in tonemapped LDR-linear space with the sRGB encode applied after, so every color target displayed ~2× lighter than authored ("deep navy" presented as pastel periwinkle) and mix amounts read wildly non-perceptually on darks (a 9% haze toward cream lifted sRGB-0.10 pixels to 0.33). Authored colors are sRGB display values, so the blends toward them now happen in encoded space (
encodedMix/encodedMixCpu, one shape across all six sites: grade haze, tint, both vignette colors, the fused WGSL vocabulary, and the CPU mirrors). A full-strength target displays as exactly the authored color; mix strength reads as display-referred opacity; sub-epsilon amounts are bit-exact passthrough, so looks without haze/tint/vignette render identically. Scalar grade ops (exposure/saturation/contrast/lift-gamma-gain/temperature) stay in linear with the compensated pivot. Merged on the jure/jacob taste verdict — every look-bearing world re-renders correctly. - Place-host wedge self-heal (#9618, ledger 1478): the client-auth place-host reset/epoch cycle could wedge — resets failing while heartbeats pass — freezing replication on a stale world copy with the host's place physics never rebuilt (static colliders dead while meshes render); a sole live-uplink host was retained hosted-on-paper forever, and only a manual room restart recovered (the field specimen wedged every 40–60 min). The wedge signature is now observed, not inferred:
resetDeliveryFailStreakcounts consecutivewire.send_failedtruth events, retired only by edges that prove recovery (socket replacement, or a live-table-corroborated host stamp reaching the drain). A sitting host with the signature (streak ≥ 5, uplink live, off Streaming) is re-assigned epoch-forward through the ordinaryassignHostrail — the same machineryrestart-roomsdrives manually, scoped to the wedged place — preferring a different Streaming candidate when one exists, with a sole candidate re-assigning itself and restarting the retention clock. Healthy paths never touch the heal; the #9394 sole-candidate timer-hold stands untouched. - BREAKING (multiplayer
parent:"player"rows): player body vocabulary — composed spec trees can BE the player (#9646, plan p-f1a81569). Every spec object tree whose root hasparent: "player"instantiates once per active player in multiplayer server rooms, parented to that player's entity and owned via the spawn chain (TomeSpawnedBystamped depth-1 on every part, so the owner's client-auth writes pass the per-row upload gate — zero latency on your own screen). Parts are ordinary AOI-replicated entities (remotes see the rig on the avatar's stream; place transfers ride the existing subtree transfer). The derivation (instantiatePlayerBodyObjects) is a pure function of (spec, live player set) run on both diff sides of applySpec — spec edits converge for every player through the existing create/update/destroy machinery; join/leave stamp and reap per player (suspension is a table stamp, never a leave). A root behavior runs per instance withapi.id= that instance's id. The exactly-one-player fallback for spec rows died, and unresolvable player-scoped parents now warn (deduped) instead of silently skipping. - The loading-curtain's worker leg (
scene.readyHint) derived from LIVE simulation conditions — current-spec-hash equality AND physics-not-paused — both of which can legitimately stay false or oscillate indefinitely over a fully running world, so the curtain could park at "Starting game… 50%" forever while the sim ticked and the renderer presented frames (ledger 1485, dump 336680ea: 3+ prod occurrences 07-17, hard-refresh/incognito/room-restart immune). An oscillating blocked state also reset the ledger-771 blocked-warn's persistence throttle every flip, which is why the wedge was silent. The leg is now a monotonic content latch (createSceneReadyContentLatch): once the authoritative spec has been applied to this mount epoch's world at least once (TomeAppliedSpecHashResourcepresent — only ever written by a successful apply), scene content is ready and stays ready; physics pause and spec churn are simulation facts that happen behind an OPEN curtain, and the gate's renderer leg (completed presented frames, self-healing per 771) still guarantees pixels before the curtain rises. The one remaining blocked state (awaiting first spec apply) has a constant fingerprint, so the 771 blocked-warn now fires reliably for it.shouldEmitSceneReadyHintandhasAppliedCurrentWorkerTomeSpec(the equality predicate) are deleted; the mount-epoch boundaries (worker dispose + mount init) reset both the latch and the hint channel so a reused worker can never mute the hint behind a stale ack. - Instrument rider (same defect family, dig receipt): the host's scene-gate flags (
rendererFirstFrameDone/workerReadyHint/domReady/sceneShown) now ride the perf rollup at the parent-relay hop (same seam as the loop census), and kiln retains them into debug dumps (sceneGateblock + aScene gate:summary line). A dump minted over a stuck loading screen now names its false flag directly — the ledger-771 console collector (__spawnCollectSceneGateSnapshot) required a live console the reporter never has, and the dig burned 20 minutes re-deriving what this one line states. - Fixed a permanent "Starting game…" boot wedge for specs with
camera: { kind: "custom" }and no runnable camera behavior (nobehaviorrefs, or refs that fail to compile — e.g. a deleted/renamed camera script). The camera behavior system used to early-return without ever publishing a camera view, so the renderer never built a frame camera, never presented a first frame, and the scene gate's renderer leg held the boot curtain forever over a healthy running world (refresh/respawn immune — deterministic from the spec shape). A scriptless custom camera now runs an empty static behavior: the authored pose publishes every tick (projection sync, pointer-lock intent, and hideLocalPlayer all still apply), boot completes, and a camera script takes over the moment one lands. One session-scoped console.info names the state. setProperty('model')RETIRES the profile-avatar fallback a3dc instead of retargeting it (#9688, ledger 1496): the fallback dresses a bare player template inDrawAnimated3DCharacterat join; when a creator's script then set a custom model plus its own locomotion, the retargeted a3dc's state machine (feature order 10001, enginerunThreshold3.0 m/s) rewrote the script's identically-keyed idle/walk/run mixer channels every tick — the character ran at walk speed and no script fix could ever take effect (Savi shipped a correct fix twice into that wall). The write seam now reads the same fallback provenance predicate the dress sites use (playerTemplateWantsDefaultAvatar, extracted toplayer-template.ts) — component removed, its engine-authored locomotion channels (A3DC_LOCOMOTION_CHANNEL_KEYS, the frozen contract) removed with it, script channels under other keys survive. Explicit spec-authoredanimated3DCharacter(trueor{ modelId }) keeps the retarget; non-player objects unchanged; matches the spec-apply seam, which already retired a3dc when a model was authored without it (engine-never-decides-anims law).- The SP run_script forward lane covers the real caller shape (#9692, plan p-d2720022 b1+b2): production Savi/wisp run_script always carries a userId, which kiln maps to a targeted entry — and the no-client server-exec lane (#9562) only served UNTARGETED entries, so a real Savi call against a closed-tab singleplayer room still fast-failed NO_CLIENT (≈244/week), and the fence-void retarget lane (#9586) stayed dark for the backgrounded case (~74% of the failure corpus). (b1) the no-client server-exec lane widens to provably-absent targets — no connection-table row AND nobody mid-join, decided at the forward system's verdict on unsent entries only; a target with ANY row (mid-join, detach grace, disconnect-save, backgrounded-alive) keeps its behavior byte-identically. (b2) the fence-void retarget lane widens to targeted entries — a hidden/frozen tab's voided offer retargets to server exec instead of merely failing fast, with the late-zombie-reply double-execution detector pinned at ≈0. Storage misroute is impossible by construction (no storage verb callable from any exec site;
ScriptQueueEntrycarries no identity); identity READS degrade honestly ({userId: null, players: 0}+ no-session log note). Forward-lane failures now carry structured verdict codes. - fx-gpu refusal telemetry names the actually-exhausted resource (#9666, the ledger 1492→1493 dig aid): an arena refusal now says whether the population/segment table (256 rows) or slot space ran out instead of hardcoding the slot ledger (the wild specimen printed "full" beside 278k free slots), and emits a one-line resident census of who holds the table. Eyes only — refused-newest, the never-cull invariant, and both relief tiers are byte-identical.
- Teaching sweeps into the GENERATION sources, not just skills (#9650, ledger 1489): the
spawn()docstring carries the spawn-residency caveat (non-resident spawns can't replicate or persist in multiplayer — seed via definePlace instead), the generated API reference documents the LIVERaycastOptionsshape instead of the dead pre-tombstone type, and the place-seam criterion (contiguous multi-storey interiors stay ONE place) reaches the PlaceMode/PlaceDef docstrings. Skill teaches: place boundary is a seam — stop teaching place-per-floor (#9645); god-mode blueprint pantries bake into the place spec instead of the shape the non-resident rail rejects (#9648); uploaded-models teachesparent:"player"bone equipment stamps per player (#9655); debugging teaches fix claims hand the verdict to the creator (#9663). - Internal/infra: headless room-host staging light-up — image build workflow + container block behind
ENABLE_ROOM_HOST(#9626); the room-host shell threads the engine identity into its socket URL so cold rooms boot on the right engine hash (#9680, ledger 1467); a staging return-without-refresh probe harness measuring the pin-bump silent-fork leg (#9690); a wang-tileset staple-family drift test (#9658). - SP forward verdict follow-up (#9701, kernel half of the #9692 consumer adoption): the fence-void retarget's honesty note is conditioned on the void's socket fact — a dead-socket no_ack retarget no longer overstates "game tab is open but hidden" when the socket is gone.
- Animated tileset frames (#9726, plan p-96b0a6ef): tilemap tilesets take
tilesetFrames(2–16) +tilesetFps(positive; renderer default 12, the sprite lane's KTX2 default) — playback is a uniform V-shift in the tilemap vertex shader (floor(time×fps) % frames) riding the two spare instance pad floats: zero new bindings, and the shift moves tileUv AND tileUvBounds together so cross-row bleed is unrepresentable (the lit-normals tap shares the one shifted UV). Absent fields resolve to neutrals (frames 1, fps 0 ⇒ shift exactly 0.0), so worlds that don't author them render byte-identically (IEEE identity pinned). The fields ride the terrain apply SIGNATURE (a frames-only edit re-applies), the zod twin in tome-schemas (updateTerraincan author them — TERRAIN_PATCH_ALLOWED_KEYS derives from the schema), and the sim→renderer tilemap chunk codec at v5 (frames as u8, fps as f32, 0 = absent on both — absent round-trips as absent), with a field-by-field round-trip def-equality pin and the stacked-atlas contract pinned: shifted frame-0 rect ===tilemapTileUvRectof the frame-major concat. Out-of-range values reject (the lane's tilesetColumns precedent), loop-only playback; fps-without-frames is legal and inert. - New
deleteStatespec mutation kind ({ kind: "deleteState", id, paths: string[][] }, pre-split segment paths) with a shared delete law (deleteStateAtPathsin state-merge.ts) folding identically in the kernel spec fold and kiln's transcription-pinned durable route. New ObjectAPI verbsapi.deleteState(path | paths)/api.deleteObjectState(id, path | paths)— dotted paths address nested keys; cross-writer deletes ride the existing client-auth unset intents (top-level and nested, no new intent kinds). The recorder now recordspatchState's top-levelundefineddeletes as this mutation kind, closing the resurrection bug (pc-b8ee37bd): deleted keys used to be stripped at record time (JSON cannot spellundefined) and reappeared on every reboot. - New
setPropertiesspec mutation kind:batchSetObjectPropertiesandsetPositionsnow record + spec-mirror their component writes (ONE mutation per object per batch, consecutive same-object mutations coalesced at record time) instead of being silently session-only and stripped by the next spec re-apply touching those defs (pc-aa7c14d3 rider). - Pointer-ray camera-derived axes bind the canonical presented camera explicitly (#9742):
syncCameraDerivedAxestook the first DrawCamera in ECS iteration order (lexicographic by entity id), so any foreign camera entity sorting beforecamera/mainhijackedpointerOrigin*/pointerDir*wholesale — every getInputRay-per-tick follower (cursor rings, placement ghosts, aim markers) froze at a fixed world position while the player moved. Selection is now explicit: the canonical presented camera first, id-sorted iteration only as fallback. Pinned by a deterministic pointer-follow-ring regression harness (WASD-style camera motion + stationary cursor + entity churn + foreign cameras on both sides of the sort order) plus presentation-contract pins: interpolation-disabled entities never receive a synthetic pose override, and interpolated per-tick movers present within the bounded playout delay. - Quota-parked sprite cooks tell Savi the budget truth (#9716, dig d1bb62ac): an art-budget park now sends an
art-budget-parkedDM mirroring the audio-quota-parked shape instead of parking silently. The audio park DM's resume line tells each cap lane's truth (#9709): the reset time is the server-named budget window end (unclamped Retry-After carried asrateLimitResetAtMs), not the clamped 1h re-probe hold — both lanes share one honest "resume loading automatically around HH:MM UTC" shape, killing the daily-cap self-contradiction where Savi promised voices back "for the next hour" over a ~6h park. - Skill teaches: sprite-variant economy — generate variants on demand instead of speculative pre-generation batches (#9722); NPC nameplates anchor by attachment instead of bounds-measuring (#9708, the EverSpark specimen).
- Spec-by-delta replication (#9712, ledger 1509): the game spec replicates as
{revision, ops, hash}on the wire — never the full document per revision. - RoomDO alarm-rail hydrate-failure ladder + failure-line self-identification (#9710, ledger 1488): frozen rooms converge onto fixed code instead of waking into the same failure forever, with rung guards from review — the cold-reset rung REFUSES while undrained ledger folds exist (never destroys the one non-re-derivable thing in DO storage; loud
alarm_ladder_reset_refusedreceipt, rail re-arms and the next wake retries the drain), and the repoint rung refuses non-module repoints. - ws close-code instrumentation (#9711, rows 1433/1463/1509 — instrument-only, zero close-behavior change): one structured
room-do.ws_closerow per server-side close ({code, reason, initiator, openForMs, sendFailures, connections}); every client socket-death path reports close info through the worker console relay into kiln's recent-errors ring, so debug-dump connection timelines carry the code per close; the client silence-watchdog kill closes with a distinct app code (4435) + reason instead of an anonymous bare close. - verify-fx-gpu-coupling pins the pending-texture skip-draw law at pixel level on a real device (#9325, ledger 1376): a declared-but-unresident/failed texture renders NOTHING, never an untextured solid — previously pinned only against stub devices, now against the actual sprite WGSL in both CPU-snapshot direct draws and GPU arena indirect draws.
- Terrain anti-tiling never fired since the #7602 lume cutover (#9338): the [2,4] anti-tiling strength band expected repeats-per-chunk magnitudes but was fed repeats-per-METER, so terrain never crossed the threshold. Strength now reads an explicit per-meter band — TERRAIN_STOCHASTIC_REPEATS_PER_METER [0.125, 0.25]: full strength at texture period ≤4m, authored look preserved ≥8m — deliberately NOT keyed on pool.chunkWorldSize (production heightmap chunks are 80m; a streaming knob must not restyle worlds). The jitter lattice hashes worldPos.xz × uvScale so it is world-continuous (terrain UVs are chunk-LOCAL meters — a layerUv lattice would re-anchor at every chunk border); taps stay layerUv+offset so strength-0 is bit-exact. Scope: top-projected surfaces only (biplanar cliff sides stay plain). New
verify-terrain-jitterL4 device gate: strength-0 seed identity (byte-identical), jitter-fires (dead-ramp tooth), and two-chunk border continuity against a seamless reference. - Texture-script param number coercion is gated on exact round-trip (#9733, pc-63fe8aaa):
parseTextureScriptParamscoerced any finite-Number()-able string, so a long digits-only payload (e.g. a 256-char encoded pixel string) garbled to a float like 7.777e+256 before the script ever saw it — creators worked around it by prefixing payloads with a letter. Coercion now applies iffString(Number(raw)) === raw.trim()(finiteness guard kept): "4"→4, "0.5"→0.5, "-3"→-3 unchanged; long digit payloads, "007", "1e5", "0x1f" stay strings. drawn-art.md's ctx.params line states the round-trip law. - Honest
maxDistance— reach semantics for knobless spatial audio (#9378, jacob's 2026-07-15 ambient-SFX report, dig 4b487916): the WebAudio inverse distance model never consultsmaxDistance, so the one attenuation knob Savi actually writes on ambient components was a no-op. Emitters that authormaxDistanceWITHOUTrefDistance/rolloffFactornow deriveattenuation: "reach"— prep folds a normalized clamped-inverse curve (1/d near field, exactly 0 atmaxDistance) into the voice gain, sends the pannerrolloffFactor: 0, and culls the voice atmaxDistance. Authoring either raw knob opts the emitter into untouched WebAudio inverse-distance semantics, exactly as before. The mode is decided at the deriver (the last place the author's optionality is visible), carried asattenuationonAudioEmitterValue. playSound/playSoundAtgain amaxDistanceoption riding the same reach semantics;audio.md's worked ambient example now teaches the complete honest shape.- One ray, one author (ledger 1518): the renderer's canonical pointer ray (seqlocked SAB, published every render frame) is now the sole producer of the
pointerOrigin*/pointerDir*input axes whenever it exists.syncCameraDerivedAxesreads it at the top of every client step — beforeinput.sampleForTick, so every sampled frame carries it — and its sim-side re-derivation survives only as the no-renderer fallback (headless runtimes and tests register no reader). Before, the derivation paired the CURRENT event's NDC with the PREVIOUS render frame's camera pose (rendererTransform SAB feedback) and overwrote the renderer's ray before each sample, so the fresher self-consistent ray never reached a script:api.getInputRaypicks made while running landed 1.5–2.5m opposite the movement direction (hunt 752d941e frame receipts, Lava Sculptor app d51a2105). Completes #9742, which bound the canonical CAMERA; this binds the canonical RAY. The oldpointer-ray-axesnetIngest system — whose post-sample writes were re-authored at the next step's top before any frame could carry them (dead writes) — is deleted; god-mode picking already reads the ray straight from the SAB (god-mode/systems/hover.ts). - Evict-proof button edges (ledger 1519): the raw-event ring writers (SAB + same-thread fallback) coalesce
MOUSE_MOVEunder pressure — past half capacity, moves merge into one pending accumulator instead of consuming slots, flushing as a single slot once the reader catches up or a non-move event needs ordering. Deltas are additive for the one consumer (input-resolution's per-tickmouseDx/mouseDy), so merged-vs-individual is consumer-identical below and above the threshold — but position spam can no longer wrap the ring and evict button/key EDGES. Before, ~4-events/s DOWN/UP edges shared 512 slots with per-pointermove traffic (which flows exactly while a button is held or the pointer is locked), and the overwrite-oldest overflow could silently drop aMOUSE_BUTTON_UP:mouseButtonsDownstuck held, hold actions kept firing, keyup pulses never arrived (the bloom growing ~1s past LMB release, felt on prod 5.2.3 too). The SAB writer's flush saturates the accumulated delta at the Int32 encode bound (±2³¹/scale ≈ ±214,748px) — a wrap-scale window is stall artifact, not motion — and a merged tail whose motion stops mid-congestion flushes on the next append of any type: late, never lost (pre-coalescing it was evicted permanently). Known residuals, named honestly: gamepad-axis spam (changed-axes × poll rate, needs a >1.4s live-worker stall with an active stick) and wheel-momentum spam (~120/s, needs a ~4s stall) are theoretically flood-capable producers of the same class and are left alone — the receipted producer isMOUSE_MOVE, and the same coalesce shape extends to either if it ever shows up in the field. - Tests: red-first pins at the transport layer (DOWN/UP survive an 8× ring-size flood in both writer classes, order preserved, delta conservation, healthy path unchanged, wrap-scale accumulator flushes saturated and sign-correct), end-to-end through input-resolution (a hold action releases and its keyup pulse fires when the release rides a flooded ring), canonical-ray authorship (renderer ray wins over the derivation; never-written channel falls back), and the publish-order pin (each tick's sampled frame carries the ray published before the step). The #9742 pointer-ring-freeze regression suite and camera-derived-axes suite stay green on the fallback path.
- Multiplayer spec resync is answered with ONE full
tome.spec.pushto exactly the asking connection — never a projection reset (the 5.2.4 farm-dusk whole-room lighting flicker, thread 1784418124, discriminator 4cb67aa5): spec-by-delta's recovery rail (spec-resync.ts) answered multiplayer gap / hash-mismatch / no-baseline requests by flagging the connectionNeedsReset, so recovering a single component value rebased the client's entire world —sweepProjectionForResetdespawned+respawned every projected entity, the renderer's lights map emptied (~170 ms fallback-rig flicker across the whole room), and the reset unhosted the place host → re-election → a second reset (the exactly-two-flickers cascade). The singleplayer answer — a targeted full push — was equally correct for multiplayer all along:adoptMultiplayerWorkerFullSpecPushalready names resync responses as a replace-class anchor (monotonic revision gate), and a queued full push survives the reattach outbox prune by design. The mode split is deleted; every resync now emits the same targeted full push. This removes the spec-traffic→reset coupling entirely — the reset lane keeps its real callers (reattach, transport drops, wedged senders, place changes) untouched, andspec-delta-journal's "an under-describing revision degrades to full-push behavior" claim is now true in multiplayer too (it used to degrade to a full projection reset). - Tests: e2e regression pin (
client-auth-spec-resync-e2e.test.ts, adapted from the discriminator's ARM A drive — red on parent behavior): every spec-push delivery class (clean deltas, full pushes, hash-mismatch, revision gap) against a multiplayer client holding 5 authoredDrawLightrows — rejection classes must recover the spec head with zero projection resets, zeroDrawLightdespawns, no light-count dips, and no place-host re-election; plus a reconnect-classNeedsResetphase pinning that the reset lane itself still delivers (snapshot rebuilds the projection, spec head preserved). Unit pin flipped inspec-resync.test.ts: multiplayer now expects the emitted full push and an untouchedprojectionState. The client-auth e2e harness gains production spec-push wiring (applyWorkerSpecPushon control ingest +registerSpecResyncCommand, registered exactly as room-runtime does). api.raycasthits sprite quads by default (jacob's default-on ruling, 2026-07-18): the visible thing is the hittable thing, no physics body required.billboard: "none"sprites test their authored world-space quad (feetPosition + WorldRotation via the pack's exact instance-quaternion swizzle +sprite.size× WorldScale × GeometryScale at the anchor, neutral atlas registration); billboarded sprites ("yaw"/"full") orient toward the RAY ORIGIN — a billboard faces every viewer, the ray's origin included, so the hit quad is a pure function of ray + replicated sprite state (observer-independent, deterministic on any simulator, and you can never hit one edge-on because you can never see one edge-on). 2D places use the renderer's fixed base poses (quad-orientation.ts). Hits carryobjectId/position/normal/distancelike any hit; the normal is the quad face sign-flipped toward the ray; outputs are f32-quantized. Opt out per call with{ includeSprites: false }(byte-identical colliders-only path, pinned). Out of reach by design: camera-attached / parallax / repeatX sprites (camera-frame poses), near-invisible sprites (the shared opacity × tint-alpha render threshold), text/sign quads (DrawText, never DrawSprite), and shape sweeps. No per-texel alpha test — the authored quad is the hit surface.- One quad math, shared: the core ray-vs-oriented-quad test was extracted from the lume render-picking module (
engine/lume/sprites/raycast.ts intersectSpriteQuad) and is now the single implementation under both the render picking path and the sim lane (tome/api/sprite-raycast.ts) — the two cannot diverge. Sharing exposed a latent sign flip in the mirror's billboard basis (right = −(worldUp×forward), opposite the vertex shader): invisible for centered quads but it mirrored the anchor origin offset, so default-anchor billboards ([0.5, 0] → oy = sy/2) picked BELOW their feet. Fixed to the shader's exact basis and pinned.Math.hypotwas also replaced with correctly-roundedMath.sqrtchains (hypot rounding is implementation-defined across JS engines — now the math is realm-deterministic by IEEE-754). - Candidate pruning: the per-place spatial octree gains a padded ray-segment walk (
SpatialOctree.queryRayInto— slab node prune + exact point-to-segment leaf test), padded by a world-level running max of hittable sprite quad radii (component hooks on DrawSprite/WorldScale/GeometryScale, mounted with the spatial-index hooks; never shrinks, so staleness only widens the walk). No O(all-sprites) scan per cast; per-candidate cost is the shared ~20-flop quad test behind a bounding-sphere reject. - Tests: realm-determinism pin (server and client worlds derive the byte-identical hit), ray-facing billboard semantics (two origins → two orientations, each facing its viewer), authored
billboard:"none"orientation, opt-out byte-equality, text-defs-never-hit, octree pruning, miss cases, big-quad radius tracking, and the shader-parity billboard-basis sign pin. - Completed terrain builds now pass a currency check before installing (the voxel stale-install flicker — tiger's 5.2.4 walk finding, pre-existing since voxel-10x #6622): a full chunk rebuild carries a SUBMIT-TIME edit snapshot; when the chunk's inputs changed while the job was in flight (routine where automata block ticks interleave replicated edit echoes into chunks a player is building in — the /zoo ponds), Phase 1a/1b installed the stale output anyway, overwriting the voxel edit fast lane's same-tick remesh. On screen: the player's edit showed for 1–2 frames, a pre-edit mesh replaced it for a full job round trip (~23 frames ≈ 380 ms on the recording), then the convergence rebuild put it back — edit-direction-agnostic (place AND clear). Both build systems now compare a completed output's
inputsHashagainst the chunk's CURRENT inputs at install time: unmarked chunks are current by construction (terrain-dirty marks are the exhaustive input-event source and park while a build is pending) and install with zero extra hash work; marked chunks re-evaluate (memoized under the mark stamp, so Phase 2 never pays twice); a stale output is discarded and the surviving mark resubmits with current inputs the same tick. Marked-but-hash-matching outputs still install — neighbour halo marks are routinely spurious under automata churn, and discarding on the mark alone would starve installs. No budgets, queues, or debounces: a correctness check, not a throttle. - Client twin guards the rendered mesh + local collider (
client-terrain-system.tsPhase 1a/1b); server twin guards the authoritative collider (server-terrain-system.tsconsumeCompletedServerJobs— a discarded build records asupersededbuild state, no failure warn/backoff). Place-level dirty flags now expand into per-chunk marks before completed jobs are consumed on both sides, so definition installs and place-mode flips are visible to the check like any per-chunk event. - Tests: deterministic A→B→A pin (full build in flight + mid-flight local edit → the stale completion must not overwrite the fast lane's newer output; red on parent), automata-echo discard + same-tick resubmit + convergence (client and server twins), and the spurious-halo-mark guard (marked-but-current completions still install; no churn loop).
- Sprite metadata hydration respects client-authored sprite fields on avatars (ledger 1517): avatars (
TomeBehaviorRef.specId: "player") now classify sprite authoredness by VALUE — the same lane as spec-less entities — instead of by the sharedspec.playerdef. Avatar runtime writes are session-local by design (a player's skin never folds into the shared player def via the persist mirror), so def-derived authoredness was structurally blind to the owning client's writes: a brain wearing a hero at an explicit gameplay-constant size (the zoo's[1.1, 1.1]) got re-stamped to the mint'sworld_heightguess ([1.7, 1.7]) by the upgrade pass (tiger's avatar small→big mid-walk, 2026-07-18). Value classification is the honest signal: hydration is the only writer of hydrated values, so any other value proves an author. Unsized avatar sprites (iso skins) still hydrate; spec-object sprites keep def-derived authoredness unchanged.authoredSpriteFieldsForDefreturns undefined for"player"; the unconditional player-def indexing is gone. - Sizing/grounding authorship is per canonical art identity (
buildSpriteValue, derive-appearance.ts): the writer's implicit whole-object merge no longer carriessize/pixelsPerUnit/anchoracross a canonical-identity change (facing/animated variants share identity and keep the carry; dotted writes restate fields explicitly and are unaffected). Without this, the engine's unconditional carry manufactured authorship the writer never expressed, and the value lane latched stale sizes across skin swaps: the zoo's farm→isoTown jump would have pinned the deliberately-unsized iso skin at the farm's[1.1, 1.1], and an unsizedspec.playertemplate iteration (the interpreter's player-diff whole-object write) would have latched each avatar's previous size. Both pinned red-first. Remaining residual, named honestly: a write that explicitly restates a stale value onto a new identity classifies as authored — which is what a restatement is. - Same disease family as the farm-dusk light flicker (spec-object light entities lack ClientEntity protection): engine-side re-derivation machinery writing over live client-owned runtime state. This change fixes only the sprite-hydration instance.
›migration notes
Worlds using material.overrides.glowTexture: true on GLB models (whole-object or per-part) regain their glow — no action needed. The override had been silently ignored since the 5.2.0 renderer swap: emissive tints authored over a base-color-textured model rendered as a flat untextured silhouette instead of glowing through the texture. Restores the 4.6 behavior.
Data-holder entities (spawned with no feetPosition) that share a tag with live-entity queries now enumerate at the world origin {0,0,0} on every query surface — api.query, and sensor verbs like nearest() and player scans, where they previously never appeared. If a data holder shares a tag with live queryable objects (e.g. a holder tagged player), give it its own tag or filter it out by state.
The fossil-save class dies: in singleplayer worlds, engine.crons and the lifecycle hooks (onPlayerConnected, onPlayerDisconnected, onPlaceStart, onPlaceShutdown) previously executed only in the server container against its divergent read-only mirror — so every persistence rail built on them (restore-at-connect, cron autosave, save-at-disconnect) read frozen mirror state and serialized it over the player's real save, and a mid-session reconnect could re-run the connect hook and wipe live progress with fossils. These invocations now execute on the client authority against the real game, which is what every creator already believed they did — no script changes needed. Behavior notes for singleplayer only (multiplayer is unchanged): (1) a cron fire is skipped, not fossil-written, when nobody is in the world or the tab is hidden — the world isn't advancing, so a skipped autosave loses nothing; (2) onPlayerDisconnected is delivered only for a mid-session disconnect whose client is still reachable — usually it isn't (the disconnect IS the client leaving), and at room shutdown/recycle it is always skipped (the forward rail goes down with the sim pump before the disconnect sweep runs), so save systems should pair it with a cron autosave (which now saves real state), bounding loss to one interval; (3) onPlaceShutdown is skipped at room teardown (logged) — the control channel is down and the mirror is not the game; (4) hook storage writes follow the player-context contract (user/${api.userId}/… via the job-forward rail) — the taught save-system shape works as-is, while cross-player/shared-key writes from singleplayer hooks are now loud script-visible rejections instead of silent fossil writes; (5) onPlayerConnected effects (e.g. a restore) apply moments after the player lands in the live world rather than being baked into the join snapshot — a brief spec-default window at join is normal.
No script changes needed. In singleplayer, a behavior's storage/llm/sparks job now executes exactly once — submitted by the world you actually play in — instead of twice (once from your live world, once from the server room's divergent mirror running the same scripts against spec-default state). Save systems built on behaviors (the taught api.updateStorage / storage:set shapes under user/<id>/…) stop being clobbered by ghost writes; nothing changes in multiplayer, in custom spec jobs, or in the lifecycle/cron lane.
No script or spec changes needed — spec.assets.metadata keeps its shape (bounds/sockets/parts entries are byte-identical to server-fetched ones; the new sprite2d entry is additive and inert to old readers). What changed is WHO derives: model bounds, semantic sockets, modular parts, and sprite 2d metadata now derive in the place host's browser (or the singleplayer client authority) and land through the new rail.assetMetadata command; the multiplayer server only validates and applies. Behavior notes: (1) an unhosted place derives nothing until a host arrives — entities keep spec defaults in the interim, exactly the place-pause semantics; (2) legacy worlds whose asset URLs the server could never fetch (absolute spawnfile.io paths and other non-/cdn/ shapes) now resolve metadata through the hosting player's own session — the same authority that renders the model — so bounds/socket coverage GROWS on old content; (3) server-side collider GLB fetches that the egress allowlist refuses, or that answer a permanent 4xx, are now terminal for the room's lifetime with one explanatory getLogs line + DM instead of an invisible eternal retry — fix the model reference (or regenerate the asset) and start a fresh session.
data-needs-cursor → data-modal: use data-modal if the element owns the screen (pause, dialog, full-screen menu); remove the attribute entirely for ambient HUD panels, which stay clickable without capturing the mouse. The old name keeps working identically (a deprecation note lands in getLogs when it's seen).
Look color targets (hazeColor, tint, vignette colors) now display as authored. Worlds whose looks were hand-tuned AGAINST the old washout (over-darkened targets, shrunken mix amounts to compensate) will read differently — darker and truer; re-tune toward the intended colors, which now round-trip exactly.
parent: "player" spec objects in MULTIPLAYER worlds now stamp once per active player (attached to that player, owned by that player) instead of resolving only when exactly one player existed and silently attaching for nobody in 2+ player rooms. Per-player gear is per-player now — resolve parts through the player (getObjectChildren(player.id), a relative child lookup from the rig root, or a tag query scoped per player), never a bare global object id: stamped instances live under deterministic per-player ids (player/<clientId>:<authoredId>, children by the slash convention). A world that worked around the old broken promise by transcribing the authored tree per player in script should drop either the authored row or the script transcription, or players wear two copies. Singleplayer is byte-identical.
Setting a custom model from a script on a player still wearing the default profile avatar now fully hands animation to your script: the fallback's built-in idle/walk/run locomotion retires with its component instead of retargeting onto the new model. If you WANT engine locomotion on a custom player model, say so explicitly on the template — animated3DCharacter: true or { modelId } — which keeps the retarget exactly as before. model: null after a runtime swap returns to the profile avatar.
None required. patchState({ key: undefined }) keeps its taught live semantics and now ALSO persists the delete (this is what the docs already promised — games that relied on deleted keys resurrecting after a reboot were relying on a bug). batchSetObjectProperties/setPositions writes made inside withPersistence/run_script now persist like setObjectProperty; outside persistence scopes they stay live-only exactly as before.
None required — terrain layers whose texture period is 8m or larger keep their authored look bit-for-bit (strength 0 is bit-exact passthrough); tighter repeats (full strength at period ≤4m) regain the anti-tiling variation terrain had before the lume renderer cutover. Biplanar cliff sides are untouched.
api.raycast now intersects visible sprite quads by default in addition to physics colliders. Games whose rays relied on passing THROUGH colliderless sprites (decor overlays, sprite props in the ray path) can restore the old behavior per call with { includeSprites: false }. Shape sweeps ({ shape: ... }) are unchanged (colliders-only).
None required for typical content. Avatar sprites worn WITHOUT a size (the 3d-sprites density contract) keep hydrating from the asset's declared world height; explicit runtime sizes/anchors written on player entities stop being overwritten. One writer-semantics change: a whole-object sprite write that changes the texture to a DIFFERENT art (facing/animated variants of the same art don't count) and omits size/pixelsPerUnit/anchor no longer inherits the previous art's values — the new art starts unsized/unanchored (and auto-sizes from its declared metadata like a fresh wear). Scripts that swap between different arts on a sized sprite and want the size kept should state it in the write (or use a dotted sprite.texture write, which keeps the other fields by construction).