engine v5.2.25
Engine v5.2.25
August 14, 2026
A patch in the Lume line.
what's new
- Big worlds with lots of detailed scenery run noticeably smoother while you walk around — the engine now remembers collision shapes it has already built instead of rebuilding them from scratch every time objects stream in or toggle their physics.
- Behind-the-scenes: when something goes wrong in a game, the team can now see exactly which engine version it happened on — fixes land faster.
- Fixed a bug where things your scripts built could come back invisible after a server hiccup — and stay invisible no matter what. The world now heals them on its own, and re-running the script that built them fixes them too.
- Savi now hears loudly when a script's changes applied live but nothing reached the saved game — instead of discovering it after a reload wipes the work.
›technical notes
- Content-keyed collider shape cache — the trimesh BVH construction churn is gone (dig df45b868). 81% of rapier's wasm time in a dense-place walk was
Bvh::rebuild_range_binnedundercreateCollider(160–185ms/s sustained): spec-level physics none↔static toggling and AOI streaming re-create mesh colliders continuously, and rapier.js rebuilds the full trimesh BVH inside everycreateCollidercall (desc.shape.intoRaw()), then throws it away. The new cache (engine/physics/rapier/shape-cache.ts) keys BUILT wasm shapes by the engine's stable content identity — bespoke/cook/terrain signature + exact realized scale + trimesh/hull lane — and shares one wasmSharedShapeacross every collider realized from the same geometry (an Arc refcount bump per collider, standard native-rapier shared-shape usage recovered through the JS glue). Colliders stay fully independent (pose/friction/groups/sensor live on the collider); eviction is LRU under byte+entry caps and restores stock rebuild-per-use behavior on the shared JS shape, so a freed entry can never invalidate a standing collider. Bench on the shipped package: 2,048-tri trimesh createCollider 1.21ms → 0.12ms (10.3×), 8,192-tri 3.97ms → 0.43ms (9.3×); the dig-shaped sustained churn frame drops 4.3×. - Every kernel log row now carries the booted engine identity (
engineVersion+engineHash) — the DD engine-version facet, kernel/container half. The container-tier env seed (SPAWN_ENGINE_SEMVER/SPAWN_ENGINE_HASH → logger base context, ledger #421/#584) already existed; this closes the realms it never reached: the workerd engine isolate (a fresh realm with empty process.env) now seeds the pair from CONFIG.engine at EngineHostCore construction, RoomShell stamps its engine identity (x-spawn-engine-* headers, durable) onto every shell record instead of only the failure-line identity block, and the container host wires shell records through the kernel logger — which also stops hookServerConsole re-wrapping warn/error shell records into lines whose message is the first line's raw JSON. Logging only; no engine behavior change. - Journal-replay husks are no longer permanent (ledger 2241). A runtime-spawned entity restored from the relay journal materializes sim-complete but drawable-free (Draw* components derive only where the spawn path executes, then replicate) and stays invisible until its manager's onSpawn re-mints it. Two seams made a sick host lane (2239 residency family) freeze that state forever, and both are closed: (1) the deferred-onSpawn drain (
tome/lifecycle.ts) DISCARDED a parked hook when its entity read remotely-simulated — but a zombie host row (named in the table, not proving) reads exactly like a live one, so the drain threw away the very re-mint it exists to deliver; the entry now WAITS and fires when authority lands on this seat (re-election, heal, migration). Deterministic-id spawns adopt live children in place and heal missing visuals, so a re-run after a genuine remote run converges instead of duplicating. (2)api.spawn()'s adopt-under-reclaim path returned with no writes for an unchanged child, so a script re-run — the one lever a creator reaches for — was a no-op on the very husks it should have healed; adopt-under-reclaim now heals visuals like any adopt, husk-gated (the spawn call declares a visual, the entity wears none) so live drift on drawing children stays untouched. - The nothing-persisted verdict — a persist:true run_script whose durable batch comes back empty now says so instead of silence (ledger 2244, dig 2ad10ad8). The chain had no honest failure path for persist-requested + world-mutated + empty-durable-batch: kiln's exec route skips its persist leg on an empty batch, studio-chat's fallback skips on empty mutations, and instance-place partition drops never reached
droppedMutations— a relay-hosted exec once applied 18 live writes, persisted nothing, and told nobody (the creator burned a full session filing the "revert"). Three legs, one contract: (1) the exec engine's instance-place partition now reports throughdroppedMutationswith areasonfield ("instance-place"— the write landed on a session TomePlaceInstance row that dies on room recycle;"persistence-filter"— the existing dig-9b58cad7 class), wired kernel→wire→kiln→chat; (2) kiln's exec route stampspersistOutcome: "nothing-persisted"on persist + kernel-provedworldMutated+ zero durable mutations; (3) run_script composes the same verdict from the underlying facts (old-kiln compat) into a structured field plus a note Savi cannot read as success, with per-reason copy naming where the writes actually live and what the durable fix is. - The behind-truth verdict field — a catchup-starved client now says so where Savi's instruments read (dig 439a5e6f, ledger 2242 fix 3). A relay client that knows it is behind the room's journal head (its welcome named checkpoint seq + head; its drain tracks the applied cursor) now stamps that one fact on both diagnostics read surfaces: the engine-bug client dump (
behindblock) and the authored-UI read receipts (look target:ui→receipts.behind, riding besidehud). The field carriesbehindSeqs(a stated LOWER bound — the head kept moving),behindSinceMs(the welcome instant; the join-time spec fetch delivered everything saved before it),behindForMs, and one honest sentence: edits saved after <t> have not arrived here — a delivery backlog, not a script or UI failure. Presentation floor so healthy joins stay silent: gap > 0 AND (≥100k seqs — ~50× the relay's own 2,048-row journal bound, ~86× under the 8.66M field specimen, so a fossil room reports on the FIRST look — or ≥30s of drain — a healthy join drains in seconds). Pull-only provider (runtime worker → wire session): costs nothing until an instrument reads. The field the fossil-room incident burned a surgical three-save Savi investigation for the lack of. - World reset re-mints behavior module instances — update-wired ears hear again after reset (r-7b0ec903). An explicit world reset drops every subscription on the rationale "producers run fresh", but a producer behind a module-scope once-guard (
var wired = {}+api.oninupdate()) was exactly the one that did NOT run fresh: its module instance survived the content-keyed behaviorCache across reset, the latch stayed set, and the ear never re-armed — update-wired games went deaf after reset (red-pinned by the #12599 adversarial review, pins in #12602). Design call: reset = FRESH BOOT — module scope resets with the world.resetTomeWorldnow sweeps the instance layer only (remintBehaviorModuleInstances): behaviorCache entries holding a compiled instance for the world's scripts are dropped, so the reset apply re-executes each factory into a fresh module. Every compile artifact stays warm by design — module artifacts, the function-factory memo (re-mint is factory re-execution, zero codegen, sealed workerd isolates untouched), source/dependency caches, and cached compile failures (no instance ⇒ no module state). The it.fails pin flipped green. - lume particle slab-bank refusals speak, and the FIFO stops starving later effects (#12401, the mechablade whole-effect vanish). A slab-bank allocation refusal now emits one console line + one
fx-arena-refuseddiagnostics event per deferred stretch (placement re-arms; announce-once lifecycle), instead of silently dropping the effect. The pending-FIFO skip-scans: the head keeps first claim on its slab class but no longer blocks later effects that could place now — early greedy effects can no longer starve whole later effects out of existence. Under-pressure eviction ("make room") is deliberately out of scope — design fork in the PR body for jure's ruling. worldPosition/worldFeetPosition— the world-frame position vocabulary (r-99ba97f9, BT dig fcdbe249 §2).feetPositionon a parented child is the documented LOCAL seat/part offset, so a script that computed a WORLD point and wrote it onto a parented rider was silently reinterpreted as local — BT's driver sat 17.5m from his own car in car-local space and the "invisible car" was one write. Both frames arrive as a bare{x,y,z}, so the bare write can't discriminate intent and flipping its meaning would break every authored assembly; the convert gets its own word instead:worldPositionmeans the WORLD point on every entity, parented or not. Writes convert world→local under the parent's current transform at write time (pivot fold and socket/part anchors included) and re-dispatch as a plainfeetPositionwrite — mutation stream, spec mirror, and persistence only ever see the canonical local value. Reads compose parent ⊗ local on the fly. The two frames the conversion cannot honestly serve refuse with both options named (bone attachments — only the renderer resolves the animated bone frame; camera-attached subtrees — per-viewer pose), and a patch that re-parents and writesworldPositionin one bag refuses rather than guessing which parent was meant. Purely additive — no existing write changes meaning, so no engine-version gate.api.raycastverdicts carry a terrain lane (BT dig fcdbe249 §3, sibling).{ verdict: true }results gainverdict.terrain: boolean | null— the data-model answer the overlap probes already speak (does solid terrain intersect the ray's segment per the terrain samplers; null = unresident voxel data, "cannot know"). A null hit over real ground now says so:hit: null+terrain: trueis "the ground exists and this cast could not see it" (terrain collider not resident in this runtime, or a blind context) — the fork between "collider absent" and "probe unusable" that cost Savi's repair session real time, made structural. Verdict-lane only; plain calls pay nothing.- Reset converges every realm to the document (r-16ed9c68, #12819). A plain reset's bump previously merge-applied an identical doc on non-authority clients — an empty diff — so client-side presentation ghosts (journal restates, orphaned children) stood through every reset. Reset bumps now ride
replace: true, and on that transition non-authority realms sweep membership-carrying world content the incoming doc does not author (world-content-sweep.ts). Doc-authored entities are preserved by the replace apply's snapshot-restore branch. After a reset, the world IS the document. - Delete propagation: destroyed spec rows leave the doc.
destroy()of a spec-declared id with no live entity early-returned without touching the document — the row survived every reset and boot, silently. Under a persistence context the row and its parent-linked subtree now leave the doc exactly like the live path. Bonus root:buildNextSpecWithRemovedObjectmissed theplaceId:objectIdqualified runtime form, so a qualified-form destroy edited nothing — fixed, live path included. api.destroyObjects(ids[] | queryFilter)— bulk destroy with honest per-id verdicts (destroyed | not_found | refused+reason; live players refuse rather than filter silently). Each id runs the full single-destroy machinery — no parallel delete path. Kills the graveyard-purge chore class: hundreds of orphans is one call, and a no-op is a spoken verdict instead of silence.