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

pinned

start herewhat spawn isfaqfrequently asked questionsthe betthe spawn bet

updates

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

pinned

what spawn isstart herefrequently asked questionsfaqthe spawn betthe bet

updates

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

engine v5.2.39

Engine v5.2.39

September 2, 2026

A patch in the Lume line.

Things your scripts hid or spawned stay that way when you leave an area and come back, sound starts on your first tap in games with their own on-screen UI, reloading inside a place brings you back at your game's spawn, a second tab on the same account tells the first one right away, a saved script or player change reaches your own game without a reload, and worlds with many rivers and ponds build their ground faster.

what's new

  • When Savi previews a custom material and a setting was spelled in a way the material never reads, the preview now says so instead of quietly showing the default look.
  • Objects your scripts spawned at runtime — village props, walls, cove pieces — come back visible when you rejoin a room nobody was hosting. Before, they stayed solid but invisible until a script touched them.
  • In god mode, clicking into the toolbar's search box while holding a movement key no longer leaves the camera flying on its own.
  • If your own character was removed from the world by a script and you press the god-mode key, the message now says what happened and that a reload brings you back — instead of wrongly saying you lack permission.
  • If your world's water uses the low-level liquid.overrides settings — depth tint distance, wave frequency, foam coverage, caustics strength — a change to only those settings now shows up right away. Before, the water kept its old look until some other water setting changed too.
  • When the player hosting an area of your world walks out of it, the things you built there no longer vanish for someone who joins right after. Before, a new player could arrive to an empty area for a few seconds.
  • A change Savi makes to your player shows up in your own game right away, not just in other players' games.
  • Sound now starts on your first tap in games with their own on-screen UI, on phones too.
  • When something your script spawned vanishes a few seconds later because a write to another player's character was refused or never answered, Savi's logs now say so — which write, why, and how many things it took with it — instead of nothing.
  • When the game drops an effect because the thing that sent it had already left the room, Savi's log reads now say that. Before, the same line said the thing "had no authority", which read like a rule about who may do what.
  • Reloading while inside one of your game's places no longer drops you at another place's start spot, falling through the dark. You come back the way a reload has always been meant to work: at your game's spawn, unless your game saves and restores position itself.
  • When the same account opens a game in a second tab or on a second device, the first one now says so right away ("This game was opened in another tab or window…") and stays put, instead of going quietly stale and cutting back in every time you switch to it.
  • Worlds with many rivers and ponds build their ground faster. Each piece of ground no longer redoes the river and pond water math that an earlier piece already did.
  • When Savi saves a script from inside the game, the objects running it pick up the new version right away — including in the world that ran the save.
  • When the server turns down one of a sprite's animation clips, Savi's logs now name that clip and show the server's reason and when the engine will ask again. Before, the log said the clip was "still generating" and left out the reason.
  • When a sprite setting such as its named animation is removed from a game's spec, sprites already in the world drop the old value when that edit is applied, instead of keeping it until a reload.
  • After you leave and come back to a part of your world, the things your scripts hid stay hidden and the things they spawned stay.
  • In worlds on the Mantle physics engine, walls and floors your scripts shaped with their own collider are now solid for players who join after they were built. Before, only the player whose game built them could stand on them.
  • When things your script spawned vanish a few seconds after it wrote to a player whose tab was in the background, Savi's logs now say that the background tab is the cause — that player's game never ran to answer the write — instead of only "no answer in time".
  • An arena your game creates on the fly keeps running when the player who opened it tabs away.
›technical notes
  • The look booth names the scripted-material keys it dropped (ledger 2931 — Savi filings 31426530 / e88b465a, app 8b2e64c7). A kind: "scripted" material's spec contract is { script, params }; ctx.constants is a raw read of that same params record (scripted-material.ts createConstantsProxy), not a second channel. Savi authored material: { kind: "scripted", script, constants: { mode: "umbrella" }, … } and normalizeMaterialSpec dropped the constants record on every path — the live interpreter and the preview booth run the same normalizer — so every look at the material rendered the script's default branch (ctx.constants.mode undefined → 'column') with no verdict, and the same spelling in the live spec (eruption-sky.js umbrella/puff hulls) renders the column branch in the world too. Not a booth-only defect and not a cache-key defect: the scripted record key already carries params (scriptedRecordKey → stableValue(params)), and the booth builds through buildScriptedMaterial per look without that cache; the "compile 5ms" was pipeline warm time on identical params. The cure at this seam is the verdict: scriptedMaterialDroppedKeys (material-utils.ts, next to the normalizer that drops them) names the keys a kind: "scripted" material carries outside { kind, script, params }, resolvePreviewPieces dedupes them across the hierarchy, and the preview's perf line carries the third note of the ledger-2879 family — (material.constants has no effect on a kind:"scripted" material — dropped before the renderer sees it, here and in the live scene; … route values through params …: ctx.constants reads the same params raw). Frames and materials are byte-identical to before; only the text changed. Session-lab receipt: scenarios/wrapper-embed/booth-scripted-constants.json (three params values → three different frames; three constants values → identical frames plus the note).
  • Standing runtime-spawned scripted primitives re-mint on the client that becomes their writer (ledger 2908). The level-triggered re-mint funnel (tome/scripted-primitive-remint.ts, ledger 1880) already covered def-less rows, but on the relay tier none of its doors opened at a moment when the standing rows were in the world AND this client was their writer: the rejoin order is spec adopt → applySpec (the first-apply sweep runs on an empty def-less population) → Ready → catchup drain (the rows land with their mint-era scriptHash; pre-seat the client is an observer, so they direct-apply) → election seats self as host. The #929 ingest system then derived them under the current source, mismatched (scriptContentSkew), and parked them as derive-mismatch — the renderer's geometry-delivery-stalled — with no door left to open (Savi filing ee86b774: 11 bld-*, 47 cinderhold props/walls, wc-* cove pieces solid but invisible until a primitive.params write happened to re-mint them). Two doors on the SAME funnel, both in tome/systems/scripted-primitive-ingest.ts: the SEAT edge — when this client's hosted-place seats change (getLocallyHostedPlaces, judged by placeId@epoch, not table identity), it runs sweepStaleScriptedPrimitiveRecipes over the recipe population, the identical call applySpec's first apply and the container's isolate-swap restore make; and the ADOPT edge — a stale row this client already writes (a create landing after the seat, or a row applied under the relay's repair bracket — a peer's live restate of a hosted row is echo-dropped by shouldApplyReplicatedEntityDirectly and never reaches the drain) is re-minted through remintScriptedPrimitiveRecipeRow when it reaches the budgeted drain instead of being derived-and-parked, the client-side mirror of the kernel server's upload-drain door (state-delta-apply.ts, ledger 1880 face 3), judged BEFORE the derive so the writer pays one derive (the mint's), not two. No new funnel, no new verdict vocabulary: the judge is scriptedPrimitiveRecipeIsStale (≡ the mismatch warn's scriptContentSkew), the single-writer gates are the sweep's own, observers keep the exact prior behavior (warn text and park included), and neither door judges without an applied spec. New greppable warn scripted-primitive-writer-remint-failed for a re-mint whose walk throws. Pinned red-first in scripted-primitive-rejoin-remint.test.ts: the dig's unit shape (first-apply sweep re-mints 0; row lands stale; observer pre-seat, writer post-seat → healed), the observer negative (a seat for ANOTHER client writes nothing), the post-seat adopt edge, the budget-queue interplay, the adjacent def-full face (a stale journal recipe restated over a fresh local mint pre-seat heals at the seat), and two controls (hash-current rows are never written; no spec, no judgment).
  • A movement key held across a click into the god-mode toolbar's search box no longer strands in keysDown (ledger 2480, third face; 59a0263e16 / #13851). Focus entered the search input from a state where no element held focus (the desktop toolbar tab takes focus, the shelf-open re-render removes that button, focus drops to body); a body → input move fires no blur, so the blur handler's isEditableElement(relatedTarget) branch never ran, no RESET reached the ring, and the input's keyup was dropped by the editable gate — the god camera kept flying with no key held and every later key typed into the search field (playtest 4, 2026-09-02). raw-capture now adds a capture-phase window focusin listener that runs the silent input reset when the arriving element is text entry (isTextEntryTarget, the one keyboard-ownership predicate); the blur handler's relatedTarget branch is deleted so the rule has one home. The window-blur RESET and the blur-into-authored-realm carve-out are unchanged. Real-browser receipt: W held flies the camera 4.7 m/0.8 s; release over the search box → 0.000 m over 1.2 s on these bytes vs 7.1 m on the parent.
  • A god-mode toggle with no player entity says so — never "requires editor permission" (ledger 2911; da03e1d6cd / #13849). On the local-authority toggle path (singleplayer, relay wire) the worker handed the LocalPlayerResource id straight to the toggle core; when the entity under that id was gone (a sweep-everything run_script destroyed a creator's own avatar root on a relay room, prod 2026-09-02 00:32Z) the permission predicate read SessionClient off a dead id, got undefined, and told the game's own admin creator "God mode requires editor or admin permissions" five times in a row. The toggle now judges the entity first, the way the kernel-tier command already does, and emits a true verdict on the same god:error lane: no player entity, your character was removed from this world, reload to rejoin. Kiln's plain-words map gets a matching row, matched before the generic pre-spawn "still getting you set up" row. Red-first in the worker tests; entity present + viewer still gets the permission verdict unchanged. Half B (a single api.destroy(id) has no live-player refusal while destroyObjects does) is a taught-contract change and rides a separate held PR.
  • A terrain-mark liquid.overrides edit now rebuilds the liquid surface (ledger 2907). The heightmap mark change-signature (tome/interpreter.ts normalizeLiquidSig, read by buildHeightmapMarkSignature for river/pond/ocean) hand-listed the liquid fields and named none of the LiquidShaderOverrides keys, so a mark whose only change was overrides.depthColorDistance (or waveFrequency, crestFoamCoverage, causticStrength, …) produced a byte-identical signature — the definition kept its revision, and TerrainMarkLiquidSystem (marks-liquid.ts), which rebuilds only when definition.revision moves, never re-ran buildLiquidOverrides. The renderer kept the old DrawMaterialOverrides until some other liquid field changed. overrides and bubbles now ride the signature generically: every finite-number entry rounded like the sibling fields (sigRound, 1e-4), booleans and strings as-is, finite-number tuples (flowDirection) rounded per entry, keys sorted — no hand-list, so a new override key cannot fall out again; an empty bag equals an absent one. bubbles is included because the magic/lava emitters read derived.bubbles; shoreSplash and underwaterFog stay out because deriveLiquidParams resolves them and nothing downstream reads the result. The raw mark already reached the installed definition with its overrides intact (buildHeightmapConfigMarks keeps every liquid key and only normalizes color), so the signature was the whole engine-side gap. Every existing heightmap signature (and its fnv revision) moves once at this version (two new null keys); nothing durable keys on definition.revision — edit ids mint from the journal's own appliedRevision.
  • Not in this landing: the ObjectAPI and kiln write gates still validate marks with the shared zod LiquidSchema (packages/tome-schemas/src/core.ts) and store the parsed value, and that schema does not declare overrides / bubbles / shoreSplash / underwaterFog — so api.updateMark / patchTerrain still strip those keys silently. Raw spec loads (the zoo deploy, DB specs) carry them. Adding them to the schema changes Savi's generated Liquid = {…} teaching (heightmap-terrain / api-reference skills), which is a decision surface; it rides a separate held PR.
  • A place-unload teardown is local — the leaving host publishes no delete for the place's authored rows (ledger 2921, Writer 1; edec9f12e7). A tab's applied doc keeps objects only for its kept scope (spec-sync filterSpecForPlace), so when the seated host of place P walked out, the re-filter emptied P and the reconcile destroyed P's rows on that machine — and the uploader shipped those despawns as ordinary host-stamped deletes. The relay journaled them as keyed tombs (every e:{id}:* head scrubbed); live peers refused them only because their seat table flipped one tick earlier, and a late joiner folding the journal inside the new host's settle window found tombs where the room's authored rows stood (def state, no children) until the turnover restate ~230–270 ticks later. Now the teardown ends loadedness on one machine only: tome/local-scope-teardown.ts marks a destroyed row whose place left this realm's kept scope (body place plus claimed host seats — the same predicate the effective-places expansion uses), the uploader's include filter consumes the mark and refuses the dead-entity row, and spawnObject voids a stale mark so a fresh instance's real death still publishes. Rows of a kept place that fall out of the doc carry no mark and publish as before. Runtime children the cascade reaps are NOT marked — who owns a place's spawned children across a host change is an open ruling (ledger 2925), not this change's. Red-first in relay-leave-local-teardown.e2e.test.ts, local-scope-teardown.test.ts, client-auth-upload.test.ts; the multi-client harness gains a specSync stack option that mounts the real spec-sync tick with head-only adoption (default stacks byte-identical).
  • The player-template diff's old side is now the template the live players were dressed from — the last completed apply's doc — not the GameSpecResource mirror (ledger 2920, part two; Savi's filing 5faedfde). applySpec's player block (interpreter.ts, "applies the edit diff between two specs onto live players": appearance, scale, physics, state defaults; plus the player behavior signature and oldPlayerRefs that feed the rebind sweep) read its old side from oldSpec = GameSpecResource. On the client that EXECUTES a template edit — the creator's relay tab running Savi's run_script — patchPlayer moves that mirror first and client-mode updateSpecResource requests no live apply (requestSpecUpdate is server-only), so when kiln's fold of the edit arrives the mirror already equals it, the diff finds no player change, and nothing re-dresses: every peer's tab re-dressed, the author's own kept the old template until a reload (DEVOID, dump ba330278: the cleared sprite.animation pin stayed on the creator's own player after part one). The old side now comes from TomeAppliedGenerationResource.specRef whenever the mirror has moved off that binding (specRef !== oldSpec); the two coincide while the binding holds, coherent mirror writes carry the binding forward, and without a record (before the first completed apply, after a reset) the mirror is all there is. Scripts stay the mirror's on both sides of the behavior signature — the scripts lane keeps its own contract (detectChangedScripts). Server worlds are unchanged in effect: specUpdateSystem restores the pre-edit baseline before applying, so the mirror and the applied generation already agreed there. Pinned red-first in tome/__tests__/player-template-edit-executor-fold.test.ts (5 of 8 red before): dropped and changed sprite keys, a state default, two edits before one fold, and an addBehaviorScripts on the executor all reach its live player on its own fold; a later unrelated fold re-dresses nothing; a peer's fold behaves as before. Also corrects part one's prose: a dropped sprite key does not reset the playback clock (buildSpriteValue reads the stripped base and sees no animation change; the sprite feature's own playback state drives playback).
  • A gesture forwarded from the authored-realm iframe reaches the AudioContext unlock lane (ledger 2512, fix 2). engine/audio/renderer.ts resumed the context only from the HOST document's pointerdown/keydown (resumeOnGesture), visibilitychange, the parent's focus, or start(). In any game with an on-screen HUD the game-UI realm iframe (realm-manager IFRAME_BASE_STYLE, inset:0, pointer-events:auto whenever game UI is visible) takes every pointer event and host-adapters/input-passthrough.ts forwards them as input.pass — no host pointerdown ever fires, so the pre-gesture ctx.resume() from start() pended for the whole session (WebKit parks it as interrupted; Chromium as suspended). Now handleRealmInputPass routes a forwarded pointerdown / pointerup / keydown to getAudioRendererHandle()?.resumeOnGesture("frame"), the same lane the host document's listeners feed (the host lane also gains pointerup: on touch WebKit the release is the gesture that counts for media). The browser propagates the frame gesture's user activation to the host document, so the resume from the forwarded message handler is allowed — verified in the browser gate (tests/browser/suites/host-audio-unlock.pw.ts, Chromium under document-user-activation-required + WebKit): real renderer, real context suspended with a parked voice, one click on the HUD inside the realm iframe → running, gesture:frame, RMS > 0 at the master bus, host pointerdowns 0 throughout.
  • A gesture refutes a stale parent blur. resumeOnGesture clears the parentBlurred latch before it resumes: the game cannot be tapped or typed into while it is out of sight, so a blur latch that survives into a gesture is stale (kiln's raw blur forwarder posted blur when focus moved INTO the game iframe — fix 1 cured the forwarder web-side; this cures the lockout for every shell and every pinned bundle). The parent's blur → suspend path and the visibilitychange lockout are untouched: a phone in a pocket sends no gestures. The desktop inversion face (click into the game = silent, click the chat overlay = sound; Grind95, Spawnmon, iz) has no mechanism left.
  • Audio suspend/resume state rides the perf rollup into the debug dump. WebAudioRendererHandle.getUnlockState() → { contextState, lastResumeSource, parentBlurred } (lastResumeSource ∈ start / gesture:host / gesture:frame / visible / parent-focus); worker-browser-host stamps it as audio on spawn:kernel:perf-rollup ({ contextState: "unmounted" } before any sound mounts the renderer); kiln's session-captures retains it, buildDebugDumpDiagnostics writes diagnostics.audio, and the dump summary prints an Audio: line. The 2512 dig found no audio state in 31 diagnostics keys.
  • A relay socket's close code now reaches the wire session (6aace814cf). In ClientRoomRuntime.onClose the call that tells the relay session the socket's close code was guarded on the live sniff field, which the socket-death teardown two lines above resets to "undetermined" — so the guard was never true and the session was never told, since the line was added on 2026-07-31. Two recorded behaviors rode that call and never ran from a real close: the keyed-heads 4400 degrade edge (a client racing a relay deploy or rollback degrades to JSON/legacy batches instead of carouseling) and the ledger-2868 foreign-close meter ([relay-wire] foreign close code=<n>). The guard now reads the wire mode captured for the close record (the ledger-2916 landing). No steady-state change; under a relay rollback the client degrades instead of looping. Red-first in room-runtime.test.ts.
  • replaceRuntimeLog bounds the folded message and data like a fresh row (ledger 2867; 1124c96200). recordRuntimeLog bounds every message, entity id and string data value at the composer, but the episode-line composer the purchase reject rail, the unhosted-spawn confession, the terrain pacing confession, the relay script-exec gate and the intent-effect drop confession fold their counts through stored the patch unbounded — so an id bounded on the first fire re-entered the ring whole on the second. Bounded at the one composer instead of at five callers. Pinned red-first in runtime-logs.test.ts.
  • The relay deny tier answers (every path answers). Under the relay tier a forwarded rail.intent that the target's simulator refused at a deny tier — a write to a server-authoritative component, or any non-set/inc kind aimed at a session anchor (another player's avatar) — returned nothing: no intent.verdict, so the sender's pending entry waited out the verdict-silence sweep (RELAY_INTENT_VERDICT_TIMEOUT_TICKS = 150) and aborted with a synthesized timeout, taking every Layer-3 effect of the invocation that carried the intent with it (BT / Gauntlet Goons: a host resetRace that placed the other human with setObjectProperty(P2, 'feetPosition') and built the lane in the same invocation watched the lane and the goons die ~150 ticks later, every reset, once a second human was in the room; ledger 2605 fix shape 3). translateRailIntent (engine/runtime/relay/relay-peer-apply.ts) now mints the deny verdict at the refusing adjudicator — denied_target, denied_component, or bad_payload for an unregistered component, the kernel handler's own codes — through the same outbound the control/lease adjudicators use, addressed to the sender by senderClientId/senderRef; the sender aborts in one RTT with the true code. Non-simulators still refuse silently (one broadcast, one verdict). The warn names the intent kind, sender and target instead of calling every refusal a destroy. What the tier permits is unchanged: the anchor branch still refuses property/behaviorOps/destroy (broader than the kernel's destroy-only check) — that widening is a permission decision held for a human word.
  • Every coded drop confesses (tome/intent-effect-drop-confession.ts, the sibling of ledger 2661's unhosted door). When a Layer-3 effect group aborts on any verdict code other than unhosted — timeout, denied_target, denied_component, basis_mismatch, not_simulator, … — its spawn previews and held emit/music/job submissions used to die with zero receipt. They now write ONE episode-bounded runtime-log row per (source entity, code) per minute whose counts move in place, stating facts only — what was written, to whose entity, which tier refused and with which code, and that the invocation's spawns were previews that expired with it: [Tome] 3 spawns from "race-director" were previews held behind a cross-client write and expired with it: setObjectProperty on "player/…" (another client's entity) was refused by that entity's simulator on the relay tier: the target is a player's avatar, a session anchor (denied_target). One of them: "goon-1". — data { code: "intent_effects_dropped", verdict, sourceEntityId, targetEntityId, intentKind, waitedTicks, spawnsDropped, heldDropped, sampleSpawnId }. No permission rule is written into the row (an engine version's words outlive any ruling). It rides recordRuntimeLog, so getLogs() and run_script's returned logs carry it. Plumbing: the pending buffer stamps sentAtTick and a drop verdict carries the answered intent (target, kind, ticks waited) through the settle queue; an invocation's effect group registers under its source entity (intent-context), and the settle drain reports each coded abort of such a group — counting the spawn previews the aborts released from its own preview registry — to settleClientAuthIntentEffects (tome/systems/intent-apply-client), which confesses. The ObjectAPI door's effects are untouched (object-api.ts is byte-identical); the sim-lease gate forwards the drop code instead of flattening it. The deaths themselves are untouched; unhosted stays the unhosted confession's story (probe register, rejoin teaching); code-less drops (claim-window, overflow), per-ref revert groups and projection resets confess nothing, as before.
  • The relay source-ownership drop line says which of two facts it saw (ledger 2919). senderOwnsSource (engine/runtime/relay/relay-peer-apply.ts, the kernel not_owner gate mirrored for every entity-sourced rail: rail.emit, rail.noise, rail.intent, rail.interactClaim, rail.enterPlace, and the terrain rails' no-membership fallback) printed one line, keyed <rail>-not-owner:unresolved and worded the attested sender <id> is not its authority (no resolvable authority), for two different situations: the source entity is not in the receiver's world at all, and the source entity is here but no owner and no place host resolve for it. On MISCAST (5.2.37) the first case fired once — a bolt's rail.emit arriving 0.7 s after its caster's socket closed, sourced at an entity the receiver had already removed — and the line's authority wording was read as "a non-host client's spawned entities have no authority on peers", which was never true on these bytes (owner = spawner; ~40 bolts applied over four minutes with zero drops). The null-authority branch now splits the way the sender-side verdict sweep already does (classifyVerdictSilence: unknown_target vs unhosted): <rail>-not-owner:unknown-source — dropped a peer <rail> from <sender> sourced at "<id>": that entity is not in this world. It was already removed here, or it is not created here yet. Nothing here to deliver it to.; and <rail>-not-owner:unhosted-source — dropped a peer <rail> from <sender> sourced at "<id>": the entity is in this world, but this view resolved no owner and no place host for it. Each key warns once per world on its own. The mismatch line (a resolved authority that is not the sender) is byte-identical, and every drop still returns nothing to apply — no rail applies that did not apply before. Pinned red-first in relay-peer-apply.test.ts (both branches on rail.emit, the three keys as three once-per-world lines, and the rail.noise/rail.intent keys).
  • A relay rejoin no longer re-homes the fresh body to its journaled place without the pose that moved with it (ledger 2915). On a fresh relay fold the client self-spawns into the default place and then drains the journal; its own envelope rows are meant to drop ("the fresh self-spawn is the truth"). The create rows dropped, but the ingest's echo-discrimination exception (engine/runtime/client/room-delta-ingest.ts rowCarriesServerStampedWrite) direct-applied any withheld UPDATE row carrying a clientAuthWrite !== "owned" component, on the kernel-tier proof that only the server can write such a component on a live entity. On the relay wire that proof is false for exactly one component: the relay tier lets the owner's own enterPlace write and ship PlaceMembership (runtime-client's relay widening), so the journaled {placeId} row is the client's OWN place move replayed — and it arrives alone, because the pose rides the motion lane, whose own rows correctly drop. The body landed in place room at the city's spawn, 1,150 units outside the cave, falling forever, sticky across reloads (Kaiju Curfew, dump 9b18af6a). The ingest hook set gains clientWrittenStampedComponentNames; the relay peer-frame lane (netcode.ts relayWireApplyOptions, live/catchup/repair and the host-stamp park drain) names PlaceMembership in it (relay-peer-apply.ts RELAY_CLIENT_WRITTEN_STAMPED_COMPONENT_NAMES), and a withheld row whose stamped component is only PlaceMembership takes the ordinary echo verdict. The whole own journal tail now drops and the body keeps the one coherent (place, pose) the spawn plane gave it — the recorded contract (relay-join.ts: "you resume at the spawn plane's verdict"; tucker 2026-08-12: the engine restores no position on a reload unless the game saves it). Kernel rooms pass no set and are byte-identical; TomePlaceInstance (the uploader's other widened name) is deliberately not in the set — instance rows are world content the fresh fold still needs. Harness fix in the same landing: relay-multi-client-harness.ts now routes own-identity sequenced frames to the join coordinator exactly as room-runtime.ts does (it dropped them, so no suite on the harness could see the fresh-fold own-row class); the whole relay family stays green on that parity. Pinned red-first in relay-rejoin-place-pose.e2e.test.ts (solo and two-client reload under both wire shapes, a plain-reload control, a live-travel control) and room-delta-ingest.test.ts (relay wire drops, kernel wire applies).
  • A relay-tier session takeover now tears the older tab down visibly and for good (ledger 2916; restores ledger 513's cure on the relay tier). The kernel tier announces a same-account takeover with a session.superseded control frame and then closes 4431; the relay tier (cf-relay relay-do.ts supersede rule) closes the older socket with the same 4431 and sends no frame. The client's close table already never retried the code on either tier, but everything else of the cure lived in the frame handler (runtime-worker.ts handleSessionSuperseded: the wall + closeRoomSocket), so on the relay tier the loser got nothing: no wall (connection-presentation.ts defers terminal verdicts to their own stories), the sim ticking on local state, every GameDB call parking on the idle relay session and rejecting at 30s with never became send-ready (game-db-relay.ts), and — because shouldReconnect stayed true — the visibility/online/pageshow recovery redial (requestImmediateReconnect) dialing the dead tab back in and stealing the session from the newer one: the two-device ping-pong the control frame exists to prevent (one identity: 32 supersede closes in 24h). Now the close record (ClientRoomSocketCloseInfo) carries wireMode, the first-frame sniff's verdict captured before the per-socket teardown resets it, and the runtime worker's one socket-close funnel reads 4431 && wireMode === "relay" (isRelayTierSessionSupersede) as the takeover verdict and runs the same handleSessionSuperseded the frame drives — same wall text byte-for-byte, same closeRoomSocket (so shouldReconnect=false kills the redial). The kernel tier's path is unchanged (gated on the relay wire); every other close code is untouched. The [gameplay-ws] closed … dump line gains a trailing wire=<kernel|relay|undetermined> so a dump names the tier per close. Knock-on by design: the headless room host's exit-3 detection (SESSION_SUPERSEDED_TEXT, do-not-restart) now fires for a relay-tier supersede too. No wire-vocabulary change — a relay-side control frame would be a decision lane. Pinned red-first: runtime-worker.socket-close-verdict.test.ts (the predicate per wire mode and per close code) and room-runtime.test.ts (the close record's wireMode on every producer; the relay 4431 hook-driven teardown schedules no reconnect and requestImmediateReconnect stays dead; the kernel 4431 record says wireMode: "kernel").
  • A terrain worker resolves the sampled marks once per terrain revision instead of once per chunk job (ledger 2918). resolveTerrainMarkEntries (engine/features/terrain/marks.ts) cached its sampled result in a WeakMap keyed by definition object identity, and every chunk job reaches the worker with a fresh structured clone of the definition (jobs/io.ts createTerrainChunkBatchJobInput, the deliberate clone-per-job design), so the cache missed on every job. The sampled path walks every river centerline (three base samples plus three prior-mark applies per spline point, up to 1,024 points per river) and every pond rim; on a world with eight rivers, nine ponds, and a spline road that is about 0.9 s of the 2.7 s lod0 batch and about 0.9 s of the 1.3 s lod1 batch, re-paid per batch under the one job worker a 4-thread device gets. A bounded (4 entries, least recently used) Map keyed by definition CONTENT now sits beside the identity cache: the raw marks plus every field the definition-derived base sampler is built from (generatorSource, generatorDependencies, seed, id, revision, verticalRange, metadata.metersPerTile). The key is the raw content and not revision alone — revision is a 32-bit fnv1a of the structural signature, and a hash collision or a hand-built definition reusing a revision number would otherwise hand one world another world's rivers. The share is opt-in through ResolveTerrainMarkOptions.samplerDerivedFromDefinition; createHeightmapHeightSampler (jobs/chunk-build.ts) sets it exactly when no explicit build config supplies heightAt, so the populate/test entry with its own sampler and every other caller stay on the identity path. Build outputs are byte-identical: the same 24 chunks (8 coords × lod 0/1/2) of the reporter's world, built with a fresh clone per job before and after, produce identical heightfield, padded heightfield, and blend samples (sha256 per array) and identical outputsHash. On the fleet box the lod0 batch of 8 goes 2.7 s → 1.7 s, lod1 1.4 s → 0.5 s, lod2 1.2 s → 0.15 s. Not in this landing: the client chunk-build liveness contract (deadline from submit across the queue, whole-batch discard on timeout, pending budget vs worker count, sim-tick deadline vs wall time — ledgers 130/143/2012) is a decision and rides separately. Pinned red-first in marks-content-cache.test.ts: two clones resolve once (sampler call counter), a new revision / changed marks / changed generator source resolve again, an undeclared caller never shares, the base entries stay identity-keyed, the bound evicts the least recently used revision, and the chunk-build sampler shares only without an explicit config.
  • The scripts diff's old side is now the registry the live entities were bound from — the last completed apply's doc — not the GameSpecResource mirror (ledger 2920 part three / 2927; Savi's filing 697c3d23). applySpec computed changedScripts, the superseded-source eviction, the old-side object behavior signatures, and the old player behavior signature from oldSpec = GameSpecResource. On the client that EXECUTES an api.setScript — the seat running Savi's run_script: the creator's relay tab, or the standing builder shell, which hosts every place the creator is not standing in — updateSpecResource moves that mirror first and client-mode requestSpecUpdate requests no live apply, so when kiln's fold of the edit arrives the mirror already equals it: detectChangedScripts is empty, nothing bumps the edit generations, the rebind sweep's def-sourced fast path reads equal behavior signatures and re-runs no entity. The compile is source-keyed and did pick up the new module (update() ran the new code), but the executor's own entities kept the OLD version's api.on ears and timers until a reload while every peer's world rebound. The old side now comes from TomeAppliedGenerationResource.specRef.scripts whenever the mirror has moved off the completed-apply binding (specRef !== oldSpec) — the same seam the player-template diff took in part two; the two coincide while the binding holds, coherent mirror writes carry it forward, and without a record (first apply, after a reset) the mirror is all there is. Object ROWS stay the mirror's on the old side (the reconcile's prevDef contract is untouched), so an addBehavior/removeBehavior on the executor's own object is a named residual of the same class. Server worlds are unchanged in effect: specUpdateSystem restores the pre-edit baseline before applying. Pinned red-first in tome/__tests__/script-edit-executor-fold.test.ts (3 of 7 red before): on a relay client hosting the place, a setScript of one of a two-behavior object's scripts followed by its own fold re-runs exactly the edited behavior (new ear hears, old ear gone, no stale subscription, no fault), the sibling behavior keeps its single ear and its update() keeps ticking, an identical re-fold is a delta-0 no-op, a non-script mirror write before a peer's script fold still surfaces the peer's edit, and a peer's fold behaves as before.
  • The sprite-atlas served-refusal report quotes the server's reason at 400 chars, the model lane's bound (ledger 2917, landed-look 29d62068 finding 1). features/SpriteAnimationFeature.ts reportSpriteAtlasRefused sliced the server's detail at 240 in the message; kiln sizes its content-gate park verdict (buildContentGateParkVerdict) to a 400-char engine quote — measurement, park, then the door out ("A different clip description or base sprite is a new asset and cooks fresh immediately."), which ends at char 318-390 — so the emitter's quote stopped before the door. The message bound is now 400 (REFUSAL_REASON_MESSAGE_CHARS); data.detail stays at 200 (REFUSAL_REASON_DATA_CHARS), which is the rail's per-value cap (engine-diagnostics MAX_DATA_VALUE_LENGTH) and the model lane's number — a longer slice there is re-cut to 197 + "..." by the rail. What this does NOT change: the row Savi reads. The rail frames the message and cuts it at 500 chars from the end; the frame plus this report's literal fixed text is 256 chars before any clip or base name, so the row holds at most ~241 chars of the reason (194 with the test's short base name, 43 with the 2917 receipt's prompt-length URL) — the door out reaches the emitter's message and not the row at this order. The test now folds the emitted report through the real rail (handleEngineDiagnosticCommand → queryRuntimeLogs) and pins what the row carries: the variant, HTTP 502, the re-probe clock, the base line, and the reason's head with the measurement in both the message and data.detail. Test fixtures that quoted kiln's retired decline sentence ("failed twice … background residue") now quote the shapes kiln serves today (buildContentGateDeclineMessage / buildContentGateParkVerdict at origin/master).
  • A served refusal on a sprite animation variant now reports per variant with the server's reason, and parks for the server's window (ledger 2917). Kiln serves every tombstone-backed cook failure as 502 + x-cook-state: failed + Retry-After (remaining TTL) + body detail (the verbatim reason: a content-gate decline, a cook kill, the re-enqueue cooldown). The sprite-atlas hydration lane (features/SpriteAnimationFeature.ts) had no branch for that shape: isTerminalAtlasFailureStatus is dead-4xx only, so the 502 fell to the transient report — coalesced per BASE asset, worded "haven't finished generating … the engine retries every 30s … do not change the asset URL", and the failureDetail the classified fetcher captured (sprite-atlas-metadata.ts / sprite-atlas-ktx2.ts) was dropped on the floor. Savi's getLogs showed sprite-atlas-missing pendingVariants: 1 for ten hours while a content-gate decline sat behind it (dump a7c7bddb). New classifier isServedAtlasRefusal (502 + Retry-After > 0) routes the failure to a per-VARIANT sprite-atlas-missing report (key sprite-atlas:<variant>, the dead-4xx branch's shape) that states what was refused, the status, the engine's actual re-probe clock (the server's window with this lane's per-URL jitter), that the base texture is unaffected, and the server's reason quoted (240 chars; the data payload carries detail, retryAfterSeconds, reprobeAtMs, failureHttpStatus, base, clip, channel). The fixed facts lead and the reason closes because the rail truncates at 500 chars from the end. The variant parks for the server's window (the pacing path was already there: atlasHydrationRetryWaitMs), is marked refused so a sibling variant's transient report no longer counts it as "still generating", and re-probes on its own when the window lifts — never a session-long latch, because the door reopens server-side (a fresh cook after the cooldown, or an admin regenerate). A bare 502 with no Retry-After stays on the transient lane. Mirrors the model lane's terminal park (renderer-asset-service.ts isTerminalAssetVerdict branch) with two differences: the model lane's terminal flag fires only on the "process-killed mid-cook" body fingerprint (a Retry-After 502 without it paces but does not teach there), and neither lane reads x-cook-state yet. Pinned red-first in sprite-animation-feature.test.ts (served-502-with-Retry-After fixture: per-variant key, the reason quoted, no re-ask at 31 s/61 s/5 min/3419 s, one report inside the window, a re-probe past the jittered window; a bare-502 control; the sibling pending-count control).
  • A sprite field the author removed from the spec now leaves the live sprite when that spec generation is applied (ledger 2920, part one). applyAppearanceProps → writeDrawSprite → buildSpriteValue merges the authored sprite OVER the live component (sprite.animation ?? existing.animation, same for fps/loop/size/…), which is right for a runtime facing swap that never restates those fields — but a spec re-apply is a complete authored state, and the merge carried a key the author dropped back from the live value forever. DEVOID (dump ba330278): the player template's animation: "idle" named no cooked sheet, the mixer's clip could not play, Savi cleared the pin (patchPlayer({ properties: { sprite: { animation: null } } }), v419 → v420), the fold ran the player-template re-dress, and the live sprite kept animation: "idle" until a page reload. writeDrawSprite now takes the previous authored sprite; a key the previous generation carried and the new one dropped is struck from the merge base before the carry, so it clears. The clear itself moves no playback clock: buildSpriteValue reads the stripped base, so it sees no animation change and the DrawSprite's own time/frame carry; the sprite feature's per-entity playback state drives playback and resolves the sheet's own clip on the next tick (it was never seeded while the pin named a clip the sheet did not have). Keys neither generation authored — a script's live sprite.animation write, a hydrated size — keep the carry. The object diff already handed applyAppearanceProps its previous authored props; the player-template re-dress hands over the previous sprite alone (options.prevAuthoredSprite), because giving it prevAuthoredProps whole would engage the visible/material lane gates and the god-mode fold right after reads the untouched live DrawVisibility as the spec's value. Pinned red-first in tome/__tests__/player-template-sprite-pin-clear.test.ts (pin removed → cleared; pin changed → moved; unauthored keys keep the carry; end to end through the sprite feature: the mixer's clip plays within one tick of the apply that drops the pin). This covers the fold path that produces a template diff — peers' clients and the kernel-tier server. The creator's own tab under the relay, when Savi's exec runs there, never produces that diff (patchPlayer moves the local GameSpecResource mirror first and client-mode updateSpecResource requests no apply, so the later fold diffs equal docs) — that is the class Savi filed as 5faedfde and stays open.
  • A host instantiates what it hosts (ledger 2921 — a 5.2.38 regression). The standing builder shell claims a seat for every spec place at bootstrap (the 08-31 standing-seat policy), but its world applied the spec filtered to its own BODY's place, so it held seats for rows it never built. A creator leaving place P had his tab reconcile-destroy P's rows; the shell took the vacated seat with no entities for P; on return the tab re-materialized P's rows from the spec and dispatchSpawnObjectOnSpawn SKIPped the hook ("that seat runs it and fans out") — a seat that never ran it. The creator saw the authored look, def state {}, and none of the onSpawn children. Second specimen (Savi filing 6138963a, app e363c7bb on .38): an authored place-root in a non-default place (level-root-02, a level-builder behavior, replicate:'place', visible:false) existed in the live room but NONE of its hooks ran — a patchObjectState sat unconsumed for 15 s (seenSeq 0) while the place was hosted the whole time; main's root ticked fine. Same mechanism. The cure is at the filter seam: the applied doc keeps objects for the body's place AND every place this realm claims a host seat in. The election glue mirrors the claim set into the world (TomeHostClaimedPlacesResource, via the new RelayPlaceElectionHost.writeSelfClaims seam; the mirror never narrows mid-room — the room-scoped reset leaves it standing until the new room's claims replace it); filterSpecForPlace takes the hosted set, and spec-sync, spec-update, reset and the effective-places expansion all read the same live set; a claim stated after the apply (the 10s re-sweep) re-applies the current revision with zero spec movement. Body-only clients (every tab, every walkabout) carry no claims and keep the one-place filter byte-identical. The shell now runs onSpawn for the places it hosts (RUN where seated, WAIT where nobody is, SKIP where a human sits) and publishes those heads, so a returning tab's SKIP folds real heads.
  • Mantle reads a scripted primitive's replicated PhysicsColliderMesh component first, and an unmapped static speaks once and holds at probe cost (ledger 2054; ca7e8b710a / #13930). A scripted primitive's authored collider() mesh lives in the PhysicsColliderMesh component and replicates with its buffers; only the sim that ran the spawn also registered it in the bespoke geometry cache. Mantle's resolveEffectiveMesh read the cache alone for bespokeMesh, so on every sim that received the entity (a joining client, the standing builder shell, a room after a world reset) each collider() static was a hole — players walked through palace walls on every non-spawning sim — and its create funnel re-ran and warned twice a tick forever (the 130k-warn/min staging storm). Rapier reads the component first (makeColliderDescs, isMeshColliderReady); mantle now does the same. A static whose mesh-kind collider still maps to nothing is held: no body, one warn per (entity, geometry identity) carrying the reason, a probe per tick so the buffers landing builds the body that tick, and the full funnel again only when the identity moves; cleanupRemovedMantleEntities sweeps despawned holds. Measured on the Imperial City spec (staging v1223) in the headless shell, 200 s: warn lines 1,282,227 → 8; sim busy 67–72% → 56–59%; p50 tick 22–25 ms → 17–18 ms; the 16 palace collider() statics realize and rays onto the ring wall hit at y=47 where the parent hit nothing.
  • The pipeline build watchdog reports a compile queue as a queue, and the not-ready census is whole (ledger 2924; f117281d01 / #13929). The stuck-build tripwire (ledger 1799) read a census capped at the 16 oldest not-ready keys and called every pending key past 30 s a wedge. On a 4-thread machine ~40 GPU pipeline builds are issued in a boot's first second and drain at 2–3 s each, so dozens of keys sat past the band while the queue moved; the keys behind the first 16 surfaced only after those landed, already 80–110 s old, and each was reported "stuck" (error class, "a page reload rebuilds it") for a build that did settle — 944 rows / 30 h across 8+ apps. PipelineCache.pipelineCensus() now returns the whole census (every not-ready entry, the pending/parked counts, the oldest pending age, a monotonic settle count); renderer/pipeline-build-watchdog.ts replaces the tripwire: a build past the band with settles inside a 30 s window is one "compile queue deep" line per episode carrying depth, oldest age, settles in the window and drain rate (info class, code renderer-pipeline-queue-deep, no DM); a key is "stuck" only when it is past the band and nothing in the cache settled across a full window. Progress is a count delta, not a settle clock, so a hidden tab reads as progress. The perf sample ships a payload-bounded slice of the entries (64) with the whole counts beside it (health.pipelineBuilds). No change to compilation. A/B on the dig's queue: 50 false "stuck" lines → 1 honest line.
  • A parked adjudicator's silence names itself (target_parked). Under the relay tier a pending client-auth intent whose adjudicator is a live peer with a HIDDEN tab never gets a verdict: the browser stops a hidden tab's frame loop, so its tome/intent-apply-client system never drains the forwarded-intent inbox. The sender's verdict-silence sweep (engine/runtime/relay/relay-peer-apply.ts classifyVerdictSilence) waited out RELAY_INTENT_VERDICT_TIMEOUT_TICKS (150) and synthesized a bare timeout, and the drop confession said only "got no verdict for 150 ticks" (Hexfall, ledger 2948: a host script's startRound read a hidden human's state, wrote matchCmd to that avatar, and spawned 29 bots and loot in the same invocation; every one was a preview that expired 150 ticks later, and Savi spent the round looking for who removed them). The sweep now reads the adjudicator's journaled visibility fact off the sender's own election fold at the deadline (RelayPlaceElection.isMemberHidden → the relayMemberHidden transport port → netcode) and synthesizes target_parked when that tab is hidden, timeout otherwise. The fact is read AT the deadline, never at enqueue: a tab that parked after the send is what starved the verdict; a tab that woke before the deadline answered. Nothing about when effects die moves — the same 150-tick clock, the same group abort, the same previews despawn; only the code and the words change. ClientAuthIntentDropCode (tome/intent-protocol.ts) gains target_parked; every consumer treats codes as opaque strings, so no other path changes. The confession row (tome/intent-effect-drop-confession.ts) carries the cause for the new code: [Tome] 29 spawns from "hexfall-match" were previews held behind a cross-client write and expired with it: a state write on "player/…" (another client's entity) got no verdict for 150 ticks: that entity belongs to a player whose tab is in the background, so the client that must answer never ran (target_parked). One of them: "bot_…". — data { code: "intent_effects_dropped", verdict: "target_parked", … }. Absent the transport port (kernel-tier harnesses, older runtimes) every silence keeps its timeout word.
  • The standing shell claims the places that exist, not the places that were authored (ledger 2921, fifth face — Cast Away b0ba6331). The builder shell's claim sweep read Object.keys(GameSpecResource.places), so a runtime-minted place — enterPlace(traveler, { placeId, createIfMissing }), an instanced ephemeral arena that lives in a TomePlaceInstance row and never in spec.places — was never in its claim set, never in its kept scope (local-scope-teardown.ts isPlaceKeptOnThisRealm: the body's place ∪ the claims), and its applied doc never compiled the arena's objects. When the only human in such an arena hid the tab, the truth relay's brain seated the beat-fresh shell there (host-authority.ts — a standing session qualifies for any place, residency-waived); the shell's replicated copy of the arena's manager carried a behavior ref that resolved to no compiled behavior, and the manager stopped for as long as the human stayed hidden, players standing in it. Three seams, one landing: (1) engine/runtime/relay/relay-standing-claims.ts — the sweep reads the EFFECTIVE place set (place-instances.ts getEffectivePlaceIds: authored ∪ every live row whose template resolves) and a TomePlaceInstance row landing or dying re-arms it (the runtime worker coalesces the row edges onto one deferred pass), so an instance is claimed as it appears instead of up to one 10 s sweep later; (2) RelayPlaceElection.syncHostPlaceClaims replaces the add-only claimHostPlaces — a reconcile that claims what appeared and RELEASES what no longer exists (the fold already read an absent claim as a release: the seat breaks, the mirror narrows, and nothing is torn down because the released place's rows already left the effective doc), so a long-lived shell's claim set is bounded by the places that exist rather than by every instance the room ever minted (MAX_HOST_PLACE_CLAIMS still bounds it absolutely; an empty reconcile is no information and releases nothing); (3) interpreter.ts computeInstancePlacesKey folds each row's kept-scope verdict into the instance-places key on place-filtered worlds — a claim on an instance changes no authored byte, so the filtered doc was structurally identical to the applied one and the delta-0 short-circuit (canShortCircuitIdenticalApply) skipped the re-apply that would have expanded the instance whole; now a scope change fails that gate and the old-expansion reuse exactly as a row epoch bump does (this also covers a body crossing between two instances). Unfiltered worlds (servers, the singleplayer authority) keep the bare row key byte for byte; tabs never claim and keep the one-place filter. Relay harness parity: the election host wires writeSelfClaims like the worker, and RelayHubDouble.assignHost states a rule-marked relay h:* verdict (the double holds no brain).