engine v5.2.38
Engine v5.2.38
September 1, 2026
A patch in the Lume line.
Placeholder shapes disappear when a model lands on them, a returning player's whole character comes back for everyone, repeated digging and filling no longer slows the game down, scripted shapes respect their shadow settings and land on the ground where they stand, big skybox uploads render at 4K, and Savi's builder stays in your room for the whole session — and a first-person camera renders the eye height its script authored.
what's new
- When a script moves many objects at once and one of the entries is shaped wrong (for example the position sits next to the id instead of inside
properties), the log now says which entry was skipped and why, instead of the call silently doing nothing. - Object previews now say when a custom material's texture lookups show as flat white because the preview booth doesn't sample textures — so a white preview reads as booth policy, not a broken texture.
- Savi's isolated object previews now say when an object's texture was not part of the render, so a preview that looks blank or white no longer reads as a broken texture.
- When a camera script moves its own eye (a crouch, a lean, a lower stance) and the engine's mouse-look camera does not follow, the game's logs now say so and name the knob for eye height. Before, the picture stayed put with no message anywhere saying why.
- A prop whose collider file can't be fetched now falls back to its own model's shape at once instead of retrying forever. If the model itself can't be fetched either, the retry stops after one honest log line instead of looping every few seconds.
- Games that send players to different rooms (a routing script, or "first open room") do that on the published game again — before, every player landed in the same room.
- Swapping a model onto a placeholder shape now removes the placeholder. Before, an object built as a generated shape and later given a
modelcould keep drawing the old shape inside the new model untilprimitive: nullwas written as well. - Scripted shapes attached to a parent and shaped to the ground now land where they actually stand. Before, a child object created together with its parent could sample the terrain at the wrong spot and float or sink by the terrain's height — and stay that way until the game was rebuilt.
- A player who drops and comes back is visible to everyone already in the room again, not just a name tag. Before, if your connection blipped mid-game and you came back, the other players could see your name floating over nothing — your character's body was missing on their screens until they reloaded. Now your whole character comes back for everyone the moment you do.
- Savi's log reads now say how old each line is — "14m ago", "since 3h ago · last 2s ago · ×6", or "before your last 6 saves" — so an old warning is no longer mistaken for a live one, and a warning that is still firing shows up when she asks for recent logs.
- Reading a player's client logs works in every kind of room again, including local dev rooms.
- Script-budget warnings now say how long a tick is in your game, so a 30Hz game is not misread as running twice over budget.
- Fixed a rare wedge where a saved library file looked updated in Savi's file tools but the running game kept using the old numbers until the room restarted. A saved script now has exactly one home in the game, so an edit that saves is the edit that runs.
- Texture scripts passed to a custom material as a parameter (for example
params: { map: 'scripts/tex-banner.js?rev=6' }) now bake. Before, the engine reported the texture script as "not found" even though it existed in the game's scripts. - Objects built from a geometry script (or custom geometry) now respect
castShadow: falseandreceiveShadow: falsethe same way plain boxes and spheres do. Before, a scripted object with shadows turned off still threw a full sun shadow. - Savi's builder now stays in your room for the whole session instead of re-joining every time she starts a piece of work — and your game keeps running while you are away. Before, if your tab went to sleep in the background, Savi's scripts could fail for many minutes with "the room had no executor," and her builder kept popping in and out while you played. Now her builder quietly holds any part of your world you are not actively playing in, hands it back the moment your game is running again, and background builds in empty areas keep a live world under them from start to finish.
- Skyboxes made from a big uploaded image now use a 4K version cooked from your original upload. Before, a sky made from an uploaded photo or panorama could look soft even when the file you uploaded was sharp.
- Digging, blasting, or filling the same spot over and over no longer slows the game down as the session goes on — the world remembers what each block IS, not every change it ever went through.
- The engine no longer reports a texture script as "not found" from one screen's stale view. When a texture is waiting on its script, the game re-fetches the script library automatically, and only the game itself — which can actually see your scripts — decides whether a script is genuinely missing.
- A first-person camera you move up or down now renders where you put it. Crouch by lowering the camera's eye height and the view drops with it, instead of the camera pulling back and tilting down.
- A look script may now spell its export as
export { look }or keep export-looking text inside a string — the same rule the material and texture lanes already followed. The.jsonrequire()in material, texture, and look scripts shipped in 5.2.37; this version keeps that rule in one place so the script kinds cannot fall out of step again.
›technical notes
- A malformed
batchSetObjectPropertiesentry now teaches instead of no-opping silently, and the batch verb's one-resolver id law is pinned (ledger 2877). The dig falsified the row's named mechanism: at 5.2.36 and at HEAD,batchSetObjectProperties,setPositions,readPositions,destroyObjects, anddestroy(ids)all resolve every target through the sameresolveObjectIdFromEntitychainsetObjectPropertyuses — a parented child's full-chain id (parent/child, the spec-children id law) resolves identically in every verb, now pinned red-then-green in the plain server world and throughbehaviorUpdateSystemon a hosting multiplayer client (spawn-in-update, batch-write-next-tick). What WAS silent: the batch shape guard dropped any entry with nopropertiesobject —[{ id, feetPosition }](property keys at the top level), a non-string id — with no warn, so a probe read back an unchanged world and manufactured "the batch verb refuses these ids" (the exact false mechanism the room's receipt filed; the room's genuine missing-target warns were an earlier epoch's cross-seat census divergence; their rows outlived the condition in the 200-row runtime-log ring and read as current to probes that did not compare row timestamps — retention, not dedupe: the warn rail throttles per signature for 5 seconds (shouldEmitMutationWarning), never for the room's lifetime, and a persisting condition re-fires after the window). The guard now counts dropped entries and teaches once per distinct signature on the same rail as the missing-target warn, naming the entry's id and its stray top-level keys. Valid calls are byte-identical. - The isolated preview booth now names the scripted-material ctx.texture() refs it will bind to its 1×1 white placeholder (ledger 2879's named residual — the MISCAST probe shape: a material script sampling
ctx.texture(ctx.params.map)previewed flat white with no verdict, and the silence read as a bake failure). The knowable subset is named: texture-script refs carried in the material's script params, and literalctx.texture("…")strings in the script source. A ref computed inside script code stays invisible from the resolve seam — that subset needs the booth's own binding census (renderer plumbing) and remains a named residual. The note rides the same preview notes line as the ctx.albedo and plain material.texture siblings; interim words marked for ravi's cut. - The isolated object preview now names the plain material textures it did not sample (ledger 2879). The booth never replicates the live primitives texture-array lanes, so a piece whose material carries
texture/mappreviews as its base-material color — white by default — and until now that render shipped with no verdict. A production filing read exactly that silence as a bake result: a healthy texture script (and a fresh minimal probe) "derived white" in the booth, and the whole scripted-texture bake lane was filed as broken. The resolution walker (resolvePreviewPieces) now collects the normalizedoverrides.mapref from every rendered piece, and the preview's perf line appends the same note grammar the ctx.albedo shape has carried since it landed: "(material textures not sampled in the booth — … faces render base-material color here, not a texture failure; check them with a scene look)". - The undeclared-orbit camera substitution now leaves a receipt when it loses authoring that changes the picture (ledger 2128, third specimen: medivhus's Veilhunt first-person crouch, ravi's dig). An undeclared custom rig that reads the look axes under pointer lock with numeric yaw/pitch state rides the renderer-owned live path:
camera-behaviorships akind: "orbit"rendererCamera and the smoother presents control-target feet +heightOffset(default 1.8) along the renderer's own look ray, so a script'ssetProperty("feetPosition")/lookAtsurvives only as a distance along that ray and the camera-sync mirror writes the displayed angles back over the script's state. The three existing teach signatures are divergence detectors with deliberately large thresholds (2.5 m off the orbit ray, a quarter turn off the echo) so the taught orbit stays quiet — which is why a first-person eye-drop (feet + 1.6 → 0.95, against a presented 1.8) never tripped any of them: nothing at any surface Savi debugged said the pose had been replaced, and five fix rounds went into real JS bugs that could not move a screen rendered from another camera. Newcamera-live-path-pose-discarded(warning class, log-only): each authored pose on an undeclared live-path rig is decomposed against the presented ray (measureLivePathAuthoredPose) into the script's implied pivot height minusheightOffsetand the horizontal offset the ray cannot carry; the receipt fires once per rig, sustained over 3 passes, when either channel moved by more than 0.2 m since the rig's first live-path pose (a crouch, a lean — a script that also patchesheightOffsetto match keeps the gap constant and earns nothing) or exceeds 0.3 m outright (an eye authored at 1.4 shown at 1.8; a half-size body's orbit still centered at 1.8). Along-ray disagreement (dolly, collision pull-in, occlusion ease) never enters the measure, so the taught third-person orbit reads gap 0 through every dolly and pitch sweep, and the taught first-person eye at a constant 1.6 reads a constant 0.2 gap under the static threshold whatever the pitch — the starters earn nothing; that constant gap is the sibling held PR's job. Delivered on the three lanes theunknown propertyteach uses — worker console warn ring, the client's runtime-log ring (theread_client_logstail), and the engine.diagnostic rail to the server ring (getLogs) — naming both sanctioned channels: camera stateheightOffsetfor eye height on the live path, or a declaredorientation: { source: "script" }to keep the pose script-owned. The per-rig latch, baseline, and sustain count live in camera-behavior's system state beside the look-axis latch and reset on a behavior swap, so a script edit that still loses a pose is told again, once. Declared rigs (either source), the god eye, and rigs that never author a pose are never measured. Outside the detectors' shared hourly throttle and never a DM. No change to the substitution itself, the classification, or any presented frame. - A dead 4xx on the asset service's collider-variant GET now latches terminal on the first attempt and runs the local visual-model cook (ledger 2893). The collider retry ladder classifies a settled failure on its structural status (
error.response.status, onecauselink deep —readStructuralHttpStatusincollider-assets.ts), never on the message. The Magic CDN probe already answered its 4xx that way (MagicCdnFetchError), and the hull extractor's own fetch did too (GltfFetchHttpError), but the asset service's payload GET behind a passing probe (AssetServiceImpl.loadModelAsset) threw a plainErrorwith the status only in its text —Failed to fetch GLTF from …?transform=collider…: HTTP 401 Unauthorized. On the container tier every root-relative lightweight fetch rejects (ambient fetch has no base), so that GET is the lane that reaches the CDN, and its 401 read null at the classifier:readTerminalColliderVerdictstayed transient, the dead variant rode the flat 5s ladder for the room's lifetime, and the local visual-model cook rescue (gated on a terminal verdict) was unreachable — the prop collided as the placeholder box all session (REDFALL, 09-01).loadModelAssetnow throwsAssetFetchHttpError(asset-fetch.ts), the sameresponse.statusshape, message unchanged. Transient answers (5xx, network) keep the existing ladder byte for byte. - The engine bootstrap reads the room mode from the surface it booted in (ledger 2898, engine half). cf-edge iframes carry the room mode only in the URL path segment (
/{engineHash}/{variantId}/{roomMode}/{roomId}/) plus the templated__SPAWN_CONTEXT__.roomMode, and the bootstrap'sbuildConnectionQueryread only?roomMode=— so every live /play boot on the edge lane read as "dev":resolveRequestedRoomIdcompared the iframe's live-room-1 against the dev default and handed pickRoomrequestedRoomId="live-room-1"withroomMode="dev"; a routing script honoring the explicit request (the builtin firstOpenRoom does the same) picked the default room, the pick equaled defaultRoomId, no second connection URL was minted, and every player landed in live-room-1 with no pickRoom failure to report (the socket stayed live only because kiln re-stamps the mode on the connection URL).buildConnectionQuery(moved toboot-connection-query.tswithisInstrumentRealm) now resolves the mode throughreadSurfaceRoomMode— spawn context first, then the strict?roomMode=read — the same readerapi.getRoomModealready trusts; client-error-forwarding stamps the same fact (its rows readroom_mode=devon every prod edge boot before). The kiln half (the edge iframe src mirrorsroomModeinto the query beside roomId and variantId) heals every already-pinned bundle at the kiln promote without a repin; this half makes the engine read the fact from the surface itself. Red-first inboot-connection-query.test.ts(edge-shaped href + spawn context reads live; local lane and no-mode fallback unchanged). - A
modelwrite on a primitive-bearing entity now releases the whole primitive, and the renderer draws a bespoke body only while itsdraw/meshpointer stands (ledger 2889). The two doors that displace a primitive were asymmetric:primitive: nullremoved DrawPrimitive, BespokeGeometry, DrawMesh, the TomeScriptedPrimitive recipe and the generated scripted collider mesh;writeDrawModelremoved only DrawMesh and DrawPrimitive, so a model swapped onto ascriptedorcustomprimitive left the recipe and the built geometry resident (derive-appearance.ts, the two-line tail ofwriteDrawModel). The renderer then resolved the still-known geometry signature after the pointer REMOVE and kept drawing the stand-in under the model (lume/primitives/store.tsreconcileBespokeRecord, thestaleDatafallback withmeshId === null) — medivhus's two-body civilians, three creator flags, two game-side fixes that measured the units clean while the screen showed doubles. Now one shared removal (removePrimitiveAppearance) sits behind both doors, and on the renderer the pointer is what draws: adraw/meshREMOVE retires the bespoke record (representation released, material intent parked for a later re-point), and a geometry row with no pointer draws nothing. Joining clients, whose stand-in geometry is derived locally and never retracted over the wire, stop drawing it the moment the pointer leaves. ctx.groundYrefuses an unresolved anchor — a parented child spawned in the same batch as its root drapes against its composed anchor (ledger 2770; kin 2835, 2592). A scripted-primitive generator callingctx.groundYon aparent:child minted while WorldFeetPosition still held the authored parent-LOCAL offset (ledger 160's eager mirror — applySpawnProperties runs before the TomeParent write) sampled terrain at the local x/z and baked ABSOLUTE terrain height into every vertex as if it were anchor-relative; the wrong samples were then CACHED in the recipe's ground tape (the 2835 determinism contract), so the drape never self-corrected, and the ground-redrape system could not heal it because its first sight arms without a re-mint. Now: the spawn lane MARKS the pre-attach window; a ground sample against a marked anchor is refused (flat degrade, run flagged) and the derive DEFERS — no geometry, no pointer, no tape, no trace — then resolves the moment the transform lands: the attach seam (composeAttachedWorldTransforms' tail — spawnparent:, setEntityParent, the attachment flip, the spec rebind pass) runsredrapeGroundRecipesAtAnchorLandingthrough the redrape system's own two mechanisms (the recordedGroundSamplesStillTrue pre-check + the shared re-mint funnel), same transaction, so no wrong pointer or tape ever reaches the wire. The spawn lane also resolves its own deferral unconditionally after the attach block, so a dynamic-bodyparent:child whose collider() never samples ground cannot stay invisible forever (physics-owned poses skip the compose seam).collider()joins the refusal law: a refused sample applies NOTHING and the post-attach physics re-run builds the real body in the same transaction — never a silent flat-0 baked into collision. The anchor-landing seam keeps the ground-redrape system's single-writer gates verbatim (an observer client never overwrites an adopted authority tape with a locally sampled one). A danglingparent:never opens the window — the standing anchor IS the world anchor, one derive. Red-then-green inscripted-primitive-parented-ground.test.ts(child trunks baked terrain-at-the-local-offset, 0.3 vs 4.3 on the curved bowl; after: anchor-relative, derive count ≤2, tape replay-true, remote client mints the pointer's exact bytes). E2E in a real dev stack: child world bounds y [4.5, 11.7] on the old bytes vs [4.5, 19.7] fixed, stable across ticks.- A rejoining member's return announcement restates its whole SESSION ENVELOPE, not just the root (ledger 2782's delivery face; jissi's Blood and Mead, 2026-09-01). When a member's wire dies, the relay's 4410 liveness sweep closes its socket and journals the membership tomb; every resident peer folds the tomb and despawns the member's departure envelope — root + tome/parent hierarchy + session-scoped spawns (relay-membership-apply). That fold is correct. The return was not symmetric: the F10 fresh redial over a live world (ensureSelfSpawn's knownEntityId branch) rebuilt only the root's create, and nothing else re-delivers the parts to an already-resident peer — their keyed heads sit in the journal but heads drain only at joins, and the reconnect re-assert (restateOwnPublishedTruthAtReAdopt) is update rows, which never rematerialize a despawned entity. The resident saw a floating name tag over no body until he reloaded (a 23-entity wobble rig: root back, 22 parts dead; three skeletons vivified by later update rows). The envelope walk now lives in
relay-session-envelope.tswith two consumers — the tomb despawn and the return announcement (buildRelaySelfEnvelopeCreateMessage) — so despawn scope and re-announce scope cannot drift apart. The F10 rebase and the tomb-shaped ghost-delete defense (reannounceSelfat envelope scope: a self-tomb or a peer's grace despawn of us) send the envelope's parts as ordinary creates BEHIND the presence-stamped root, control-flagged past the wall-5 admission (an F10 lands exactly when the outbox may still hold the dead wire's backlog). The root create keeps its exact bytes, so the election fold's input is unchanged; the parts ride the keyed re-chunker as their original mint did (scrub-and-insert upserts — idempotent at a peer that never lost them). The peer-join delivery repair keeps root scope (the joiner drains the parts' keyed heads itself), so join-edge wire cost is unchanged; a bare avatar's return is byte-identical to before. Two adjacent findings named, not built:prepareFreshRedial's keyed-outbox prune destroys never-journaled one-shot rows minted during a dead-wire window (world-scoped writes outside the envelope still die silently on the F10 path — the envelope re-announce now covers the envelope's part), and a baseline-less "skeleton" materialization detector would make this class queryable fleet-wide (the materialization path at the specimen's pin was not pinned from master bytes this turn). - Every answered relay exec writes one executor-attribution line into the answering world's runtime log ring (ledger 2875, instrument half). The exec answer on the wire is
{execId, result}and names no executor, so during a headless-shell treadmill (2863(a)'s supply: the exec seat moving every 30–60s) nothing recorded WHICH WORLD answered a probe — a young shell honestly answers young ticks while the creator's tab answers ~305k, getObject/getPlayers describe the shell's mirror, and three digs (2871, 2873, 380c1c11) each spent hours re-deriving "who answered"; dig 383de524 named per-probe executor attribution structurally unrecoverable from the record. The one accept path's resolve now records[relay-exec] answered by <executor>: execId=<id> tick=<t> railBirthTick=<t0> railBirthAtMs=<ms0> specAdopted=v<n> specApplied=v<n>— the executor session identity, the answering world's rail-birth anchor (wall clock + own tick the first time its exec rail ticked; a headless shell's boot, within a tick — named for the rail because a tab that ran singleplayer first joins the rail on the same world, later than boot), the currentTick at answer, and BOTH version facts dig 2872 found conflated (the adopted spec head, TomeSpec.dbVersion, moves on adopt; the applied resource version, TomeAppliedSpecDbVersionResource, moves on apply — the result's readAtVersion stamps only the former;specApplied=none= no versioned doc applied since boot). The ring row is warn-tier (the dump's server tail keeps warns) and FOLDED per (executor, adopted, applied) episode — consecutive answers by the same executor at the same versions update one row in place (latest execId/tick, ×N since the episode's first tick), because a per-exec row is a unique fact the ring churn guard can never fold, and measured prod exec peaks (26/min on one session, 36.5k/day fleet-wide) would evict the 200-row ring in ~8 minutes and the 80-row dump slice in ~3. The console copy stays per-exec (run_script, readOnly probes, agent-lane execs; failure answers included) at info tier — shells' winston ships info to DD, and a second per-exec WARN would saturate the tab worker-console relay's 20-warn/min budget the accept line already half-spends — and the stem[relay-exec] answered byis the verbatim-count contract. Bounded length; never the script source or the result payload. The response wire shape Savi reads is untouched (the surface half is a separate held decision), and seat selection/routing are untouched. - Log ring rows render legible time on both tool surfaces (ledger 2877).
getLogsview rows andread_client_logs' log tail + console rings carried epoch-ms at best, so a warn fourteen minutes and six saves old read as CURRENT. Every reader-facing row now wears one rendered age string minted at read time on the realm that wrote the timestamps (renderRuntimeLogAge/renderConsoleRingAge, runtime-logs.ts — one rendering serving bothqueryRuntimeLogsand client-sight): plain rows14m ago; churn-guard folded rowsfirst 22m ago · last 14m ago · ×6; rows older than the room's applied spec add· before your last 6 saves(game_specs.version increments by exactly 1 per durable save, so the version difference IS the save count); collapsed console rowssince 14m ago · ×6(the ring knows only the run's first occurrence). Episode rows written viareplaceRuntimeLognow stamp both anchors —timestampMsstays the episode's base (the anchoring rule is untouched),lastTimestampMs+countmove with each fire — and rendersince 3h ago · last 2s ago · ×6; every replaceRuntimeLog caller (relay-exec attribution, chunk rescue, purchase dispatch, unhosted-spawn verdict) gets the last anchor through the one writer.getLogs'sincemeans ACTIVE since: a row is admitted when max(timestampMs, lastTimestampMs ?? timestampMs) >= threshold, so a still-firing episode is no longer invisible to a since read while its base anchor is hours old; a truly dead old episode still filters out. Machine fields are additive (lastTimestampMsridesRuntimeLogViewbesidetimestampMs); rows are byte-identical otherwise; the container-tier answer sanitizer passes the age field through bounded (worst render 66 chars inside the 80-char bound). - The container tier's control door admits clientsight (ledger 2877, e2e miss). The container host's hand-copied CONTROL_PATH allowlist (update|exec|placeholders|reset) predated client-sight, so
POST /rooms/:id/clientsight404'd at the container front while the RoomShell behind it has served the door since it landed —read_client_logsagainst a container-hosted room (every local dev room, the prod floor tier) died as "Not found" one hop before the handler. One-word allowlist fix; verified e2e (the rings answer through this route). - Watchdog budget lines name the tick the budget is half of. The behavior watchdog measured a script against "its 16.7ms tick budget"; the value is half a tick interval by construction (
resolveBehaviorBudgetMs), so in a 30Hz game the line read as a 16.7ms tick and a creator's Savi priced a room holding 30Hz as 2x over budget (Broodfall filing e94d2836). Every Savi-facing budget line now carries the tick length and rate — "its 16.7ms script budget (half the 33.3ms tick at 30Hz)"; a fold-scaled budget names the interval it was granted, a clamped budget names the tick without claiming to be half of it. One helper (describeBudgetMs) owns the wording across the breadcrumb, both park shapes, the client-simulated diagnostic, the sustain-law verdict, and the physics step lane. - One registry slot per script (ledger 2872, savi filing 32ac35c2 "Back Fire Spins"):
GameSpecResource.scriptscould hold TWO spellings of one script (canonicallib/pusher-spec.jsbeside ascripts/lib/pusher-spec.jsvariant), and the readers walked the pair in different orders — resolveScriptSource (require, behavior compiles, dep-watch freshness) probed the canonical slot first while getScript's coerce folded spellings last-wins — one registry, two readers, two answers, and saves never invalidated because the diff keyed on the raw spellings. Every write seam now collapses key spellings to the canonical form ONCE at insert, later write wins (the durable-doc fold rule kiln's coerceScriptRegistry established): applySpec's sanitize pass, foldSpecMutations (server fold lane, client adopted-mirror folds, relay fold-rebase), updateSpecResource (every ObjectAPI doc write), the spec-update drain's whole-world record, and the client sync apply's merged doc. The readers collapse to one view: resolveScriptSource and the require key resolvers probe exactly the canonical slot (ref-spelling tolerance stays; registry-spelling fallback probes are deleted), so the split state is unrepresentable rather than tolerated. A collapse at any seam logstome.scripts.key_spelling_collapsednaming the raw keys — the tripwire that pins the writer if a variant spelling ever arrives again. - Exec results stamp BOTH spec version facts (the dig's instrument gap):
specVersionstays the ADOPTED head (TomeSpec.dbVersion — it keeps climbing while a stalled fold applies nothing) andappliedSpecVersionis new — the APPLIED dbVersion the exec's world actually interpreted. The pair diverging is a fold-tracking stop, visible directly on the next specimen's run_script result instead of needing a live-room dig. - Texture-script refs carried as scripted-material PARAMS now ship in the TextureScripts library (ledger 2879, second face; production filing dad6d147, MISCAST). A material script that samples
ctx.texture(ctx.params.map)receives the ref as data, so the library selection could not see it anywhere: the spec walk visits texture fields, the script-source scan only matches literalctx.texture("…")strings, and no component hook readsscript.params. The library never shipped the script, and the renderer's bake verdictedtexture-script-compile-failed: "… was not found in this game's scripts (waited 30s for it to be written)"for a script that exists and reads fine — a false teaching that sent the creator's session bisecting a healthy script. The selection now visits scripted-material params in every lane that already visits texture fields: the spec asset walk (both the authored{ kind: "scripted", script, params }shape and the persisted reader-echo{ key, script: { ref, params } }shape), the live DrawMaterial walk and its ensure hooks, and model per-partsubScripts. Param strings that are notscripts/refs select nothing; URL params never needed the library. properties.castShadow/receiveShadownow hold for scripted and custom primitives exactly as for plain primitives (ledger 2886, Savi filing f2cf4b07 on Blob Wars). Both kinds render as bespoke meshes, and the renderer's bespoke-mesh records never received the entity'sdraw/shadowintent:setPrimitiveShadowIntentre-homed only plain primitive records, the bespoke lane split hardcodedcastShadow: !overlay && !transparent, the direct texture pools did the same, and the scripted-material claim for a bespoke mesh passedcastShadow: truefor every non-liquid — so a scripted primitive withcastShadow: falsecast a full sun shadow beside a plain box that obeyed. The intent now lives in ONE store-level map read by whichever record draws the entity (primitive lane, oversized pool, scripted claim, bespoke slot, bespoke pool), through one statement of the cast/receive rule (shadowLaneFlags: overlay never casts or receives, transparent never casts, the intent gates the rest). Holding it at the store rather than on a record also closes two silent drops: the spawn-frame handler order (shadow collects before mesh) and a plain↔scripted primitive swap, which replaces the record without re-sending the unchangeddraw/shadowcomponent. A re-derive (geometry script edit, param write, remint) keeps the flag by construction. Pinned red-on-parent in the primitives store and scripted-lane suites; receipt:.screenshots/scripted-castshadow-before-three-shadows.png→.screenshots/scripted-castshadow-after-two-shadows.png(session-lab scenarioscripted-castshadow).- The standing builder becomes a SESSION-SCOPED resident seat, and a starved place seat is replaced on EVIDENCE of death (ledgers 2828 + 2863 as one policy — jacob's 08-31 delegation, gavi's decision). Three organs move together. (1) THE ROOM YIELD DIES on capable engines: the dev builder shell no longer exits when a creator's tab ticks (the summon/yield treadmill — exit 7 per etiquette step-aside, resummon per savi activity: 67 boots/69 min on the 2863 specimen, 1,133 shell joins in one evening on the 383de524 room, death receipts dying with each 30-second authoring seat per 2874). The shell supervisor probes the pinned worker's fold (
relay.standingSeat, the relay.selfDepart capability precedent) and stands the yield down only where the per-place seat machinery is proven; old pins keep the treadmill byte-identical. One shell now serves the whole savi session — its log ring, its onDestroy/raze receipts, and its wisp work survive with it. (2) THE ROOM-SCOPED FLOOR at the relay brain (cf-relay host-authority): a standing session's candidacy is residency-waived — beat-fresh, it qualifies for ANY place, still ranked strictly behind every real candidate — and the staleness sweep judges the union of assigned places and every candidate's stated place. A human seat whose sim-beats stop for the 15s window while the shell is live is REPLACED (2828 spelling ii — evidence of death, never presence), and the returning tab's first beat takes the seat back through the existing sitting-shell carve-out. The engine fold honors the relay's rule-marked shell rows wherever the shell's body stands (the roaming-body face of the 4510d4f8 F1 cure — refusing on the folded body place handed the seat back to the parked resident through the liveness-blind derived floor). (3) EAGER SPEC-PLACE CLAIMS: the slotted dev builder claims every spec-declared place at bootstrap (bulkclaimHostPlaces— one refresh restates the set) plus a low-rate idempotent re-sweep for places authored mid-session, so empty sibling places hold a live host for the whole session (wisps' persist scripts stop starving onroom_host_summon_failed, and a player crossing into an empty place lands in an already-simulated world — the 03963e6a face). Claims stay the floor: a resident real player outranks every claimant. Multiplayer rooms with live beating humans are pinned unchanged (never-migrate holds; the shell never outranks a fresh resident), the container tier is untouched, andspawn clientwalkabouts keep their slotless per-boot lanes. - The equirect skybox max-res upgrade now fires for the rooted
/cdn/…ref spelling (ledger 2888).upgradeSkyboxTextureRef(tome/skybox-texture.ts) gated onref.startsWith("cdn/"), but the chat tools write sky textures into specs as/cdn/public.<b64>.png— the spelling every other engine cdn path accepts alongsidecdn/…. The slash form skipped the upgrade, so the sky was requested as the 1568-capped canonical KTX2 and themax-res-4kvariant (3840 cap, cooked from the preserved raw, engine 5.2.13) was never asked for: an 8192×4096 upload and a 4k upload drew identically soft. The gate now matches both spellings and keeps the authored prefix, so the two apply sites (atmosphere-sync's DrawSkybox textureId and spec-assets' manifest id) still agree. Explicit tiers (.max-res-4k/.max-res-8kbefore the extension), query/hash refs,.ktx2and non-image refs pass through untouched in both spellings. Receipt on a local stack: before, the engine fetched…/cdn/public.<b64>.jpg.ktx2and the served KTX2 was 1568×784; after, it fetched…/cdn/public.<b64>.max-res-4k.jpg.ktx2and the served KTX2 was 3840×1920 (a 4000×2000 source). - Terrain edit journals fold overlapping voxel FILL commands — the "TNT gets laggier every blast" class dies (ledger 2890; Savi's filing 63e3c0e8, CRATERCRAFT, 5.2.37, client-hosted room). The durable-row fold (
edit-compaction.ts, ledger 790) recognised only single-cell voxel-set commands: one fill box (avoxel-setwith a non-degenerate bbox) ended the foldable suffix, so a chunk whose whole history was fill boxes never folded — the filing's 197 KB_spawn_chunksrow was ~14,000 overlapping 5×1×5 air fills on a 4,096-cell chunk, re-encoded whole on every flush. The fold now covers the maximal suffix of voxel-cell commands (single cells, fills, stamps) with one rule — later commands win per cell, then the final field stamps as palette+RLE over its tight bbox, Y-slabbed under the per-command cell budget. Nonzero-state cells still ride single-cell commands after the stamps (the original command verbatim when a single-cell set wrote them, a synthesized one when a stateful fill did). The stamp encoding is canonical (scan-order palette), the fold rewrites only when strictly smaller, and it stays at the durable save/load seams — syntheticterrain_compact_*ids never enter a live journal, so the relay tier's editId-keyed rebase is untouched. Pinned red-first: the filing's shape encodes to 220 bytes (parent: 2.8 MB) and rehydrates to the identical field; a 40-seed property test over random fills/singles/stamps/air/state; fixed-point and canonical-payload pins. - The LIVE journal is bounded too — shadowed-command pruning at the region append seam (
edits.tspruneShadowedVoxelEditCommands). A command every cell of which later voxel-cell commands rewrote changes no cell's final value; the region append now drops exactly those, amortised by a doubling watermark (floor 64 commands), so the journal stays within 2× its shadow-free size — O(cells the chunk holds), never O(lifetime blasts). Survivors keep their order, objects and ids (nothing synthetic enters a live journal). Measured on the filing's shape (14k fills, one chunk): per-fill cost at the 14,000th fill 44.5 ms/100 → 18.7 ms/100 (first 100: ~9 ms either way); durable encode per touch 66 ms → 0.8 ms on the bounded journal (11 ms once for a pre-existing 14k row); boot rehydrate of the row 28 ms → 0.36 ms; row 2.8 MB → 220 bytes. Pinned: 14k fills through the real fill funnel end within the bound, and an interleaved fills/singles/state run replays identically to its unpruned twin. - Read-compat is structural: rows decode by command content, and stamps have been a journal command since ledger 790. No storage-format change, no migration; existing raw rows fold on their next save.
- The texture-script not-found verdict moves to the sim authority rail (ledger 2879 axis 4, ravi's routing word; the shape is ledger 1307's material-script twin). The renderer's TextureScripts library is a consumer-indexed snapshot, so a bake-time miss there proves nothing about the game's scripts — yet the renderer's 30s-grace report asserted
was not found in this game's scripts … create it with the Write tool, a false teach whenever the miss was a lost forward, a never-collected runtime wearer, or a starved spec-apply lane. Now: at miss time the ref still parks immediately and the renderer fires a debounced, resolve-gated first-wear re-request (render.textureLibraryMiss, the texture twin ofrender.materialLibraryMiss) — the sim re-derives and force re-forwards the library, healing the delivery faces in about a second. If the grace expires with the family still absent, the renderer reports only the snapshot miss (texture-script-library-missing, log-only) and sends the post-grace phase; the sim then adjudicates existence against spec.scripts (reportTextureScriptMissVerdicts) and fires the authoritative not-found verdict (texture-script-compile-failed) once per ref per room, reconciled on every derivation so a healed-then-redeleted ref re-reports. Interim savi-facing sentences on both slots are marked for ravi's cut. - An undeclared custom camera on the live orbit path now renders the eye height its script authored (ledger 2128, the first-person eye-height specimen). The renderer-owned live path (
isMouseDrivenOrbitCustomCamera) presentsfeet + heightOffset + dir · orbitDistand readheightOffsetfrom camera state, config, or a fixed 1.8 — the position the script wrote withsetProperty("feetPosition")was consulted only for its distance. The first-person starter authors the eye at feet + 1.6 and rendered at feet + 1.8 (measured: crosshair ray origin at feet + 1.800 with the authored camera at feet + 1.600), and a crouch that lowered the authored eye to 0.95 moved nothing on screen — the camera dollied 0.85 m back along the view ray and, past the first-person blend band, the orbit lookAt tilted it down (Veilhunt, 09-01: "tilts the camera down but never moves it down"). camera-behavior now folds the authored position into the presented orbit by inverting the presentation's own formula: the authored offset from the control target's feet is projected onto the presented ray's horizontal line to recover the orbit distance the script meant (0 for a rig that stands the camera over the feet), and heightOffset is the authored y minus that distance's rise (foldAuthoredCameraHeightOffset). The taught third-person orbit (camera at feet + HEIGHT_OFFSET + dir · dist from the same state yaw/pitch) folds to its own HEIGHT_OFFSET exactly, so its presentation is unchanged; a rig tuned to a different HEIGHT_OFFSET now presents at that height. Precedence: an explicit camera-state or configheightOffsetstill wins; a rig that never authors a position keeps 1.8; an authored framing off the presented line by more than the teach rail's epsilon (spectate, free-fly, lock-on — the substitution family the rail names) leaves the substitution as it was; declaredorientationrigs are untouched. Retention: the folded eye height PERSISTS until the script writes a new position or the rig changes — a pass with a position write (authoredFeetPositionon the camera API handle, the per-pass sibling ofusedLookAt) refolds and replaces the held value, and the hold is keyed to the camera entity and cleared on a rig swap or a declared-orientation switch, so a transition-only writer (a crouch handler that sets the eye once at the change) keeps its eye instead of popping back to the default, and a stale write can never outlive its rig. Measured on the first-person starter: standing eye 1.800 → 1.600, crouched 1.800 → 0.950, both equal to the authored value. - How a required script module becomes a value now has one home (ledger 2903; ledger 2866 was the same disease one day earlier). Six compile lanes turn
require()'d library modules into exports — the tome sandbox (tome/script-modules.tscompileModule), the terrain generator worker (generator-runtime.ts), the voxel template lane (voxel/template.ts), and the renderer's material, texture-bake and look lanes — and each carried its own copy of the ".json → JSON.parse, everything else → CommonJS factory" rule. The renderer lanes were written without it, so a material whose lib chain requiredlib/data/palette.jsonat module scope handed the JSON text to the JS parser, died "Unexpected token ':'", and fell back to Std/PBR wearing a compile error that named a healthy file, while the identicalrequire()worked from a behavior script (Savi filing 6c0ee193, MISCAST on 5.2.36). The 5.2.37 cure mirrored the branch into four more lanes; this landing deletes the copies.tome/script-module-instantiate.tsnow owns the rule (isJsonModuleRef,instantiateScriptModule) and every lane routes through it with its own JS codegen — the tome lane's realm injections, artifact cache and export rewrite are unchanged, the terrain lanes keep the function-factory memo and loop budget, the renderer lanes keep plainnew Function. The three renderer lanes also collapse their byte-similar resolver + export strip + require closure into onecreateScriptDepRequire(lane identity is two hooks: the lane's builtin namespaces and its not-found teaching text). The look lane's strip was the drifted copy — not string-aware and not list-aware — so a look script (or its lib) with export-looking text inside a template literal was rewritten andexport { look };was a syntax error; it now runs the same string-aware strip as the material and texture lanes (ledger #913 invariant 3). A guard test (script-module-instantiate.guard.test.ts) mechanizes the census: any source that mints a(require, module, exports)factory must instantiate through the shared rule or sit on the entry-only allowlist with a reason, and no lane may carry aJSON.parsebeside a module factory — a seventh copy cannot be written without going red. Receipt (session-labwrapper-embed/scripted-material-json-lib, real renderer, native GPU): the ledger-2903 chain renders the palette's magenta on this tree (.screenshots/scripted-material-json-lib-after-magenta.png); the last pre-fix master renders the Std/PBR fallback (.screenshots/scripted-material-json-lib-before-fallback.png).