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 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

Lumeengine v5.21mo agoConnectionengine v5.12mo agoFor Realengine v5.02mo agoAtelierengine v4.62mo agoSurface Tensionengine v4.53mo agoSolidengine v4.43mo agoGroovyengine v4.33mo agoContinuumengine v4.23mo agoFoundationsengine v4.13mo agoGenesisengine v0.13mo ago
← All posts
← All posts

engine v5.2.30

Engine v5.2.30

August 19, 2026

A patch in the Lume line.

Thirteen fixes in one: quiet saves for multi-area games, touch-ready script buttons, taps that land on bridges and platforms, terrain edits that persist, smoother joins on busy rooms, no tab-return flashes, and steadier building while wisps work.

what's new

  • Saving no longer freezes or respawns objects in multi-area singleplayer games. In a singleplayer game with multiple places, every save was silently destroying and respawning every object outside the player's current place — re-running spawn effects, resetting runtime state, and freezing the game for a moment while materials and geometry rebuilt. Saves are now quiet: nothing that didn't change is touched.
  • Script-wired buttons and touch pads now work on touch — no bridge or workaround needed, even with engine touch controls on. Controls you wire from a script (element.onclick = ..., pointer handlers, cursor:pointer styling) now take the touch everywhere they're visible — the engine's movement stick and camera no longer steal taps from them, and iOS taps that Safari tried to cancel still land. Empty screen space still belongs to the engine stick and camera.
  • No more "Connection lost. Reconnecting…" flashes when you tab back into your dev room. A hidden tab's queued background traffic now just drains on return instead of forcing a fresh reconnect and a full world reload — tabbing away and back is seamless again.
  • Place travel works reliably on busy rooms. On rooms with lots of join/leave churn, a player's enterPlace could silently do nothing — the call returned the destination but nobody moved — until the next clean rejoin. Travel now lands on the first try regardless of how churny the room was when you joined.
  • Sounds can now stick to the thing that made them: playSound(clip, { position, follow: true }) keeps a sound on a moving object instead of leaving it behind at the spot it started — your own footsteps, engine hums, and whooshes stay with you instead of trailing behind as you run.
  • Spatial audio now always hears from the camera you actually see. Games with custom mouse-orbit camera scripts could previously get subtly wrong or mirrored panning while everything looked perfect — that seam is closed.
  • Safari no longer eats all your memory while a game sits waiting for its first click — the bug that could freeze a whole laptop on a quiet world is gone.
  • The erase brush now erases every painted ground material at once — one pass returns the ground to its natural look instead of only removing the material you happen to have selected (paths and roads stay).
  • Material brushes have a new Replace mode next to Paint and Erase: painting in Replace makes your material win by pushing the other painted materials out from under the stroke, instead of blending in on top. Plain painting is unchanged — it still layers additively, and brush strength still decides how strongly you cover the world's natural ground.
  • Savi gets interrupted less while you build. Routine engine notices — a texture that didn't load, a clip name that doesn't exist, a placeholder holding while art generates — now go quietly into her logs instead of pinging her mid-conversation. She still hears immediately about real breakage (broken scripts, crashes, things that stop rendering), and about any problem that suddenly happens dozens of times at once.
  • Games with lots of moving objects spend less engine time per frame on internal bookkeeping — positions, rotations, and scales are read without generating garbage-collector pressure, leaving more headroom for game logic at the same object count.
<!-- falsifies: none. Every change is engine-internal: behavior scripts and run_script observe query results only through api.query / api.getSpec / ObjectAPI reads, which build their own fresh result objects at the boundary (verified: query-utils.ts buildQueryResult, the exec overlay's own row builder, and per-call world.get mints). No script-observable behavior differs, so no discriminating probe can exist. The section is omitted rather than emptied because the parser accepts only claim/probe items under a falsifies header. -->
  • The stray scrollbar that could pop over your game's right edge (and make your game UI look broken) while art was spawning is gone.
  • Editing your game's UI no longer gets stuck showing the old version until a restart. If the room's update lane ever stalls, the engine now notices within a few seconds that your HUD script changed, recompiles it from the live copy, and repaints — and tells Savi it happened instead of silently serving the old screen.
  • Savi stops getting blamed for axes the starter template ships. Every input change in a 3D game used to print five warnings telling her to remove lookX/aimYawSin-family entries that the platform itself put in the game — noise that could bury the real error when something was actually wrong. Those entries are now recognized as the platform's own, new games no longer include them, and the warning only fires for something that can genuinely break: binding a key or stick to an axis the engine computes from the camera.
  • Changing another object's state now just works everywhere you can see it: edit .state on anything a query or a collision hands you and the change is real — no more edits that silently vanish because they touched a copy.
  • Bot-heavy and query-heavy games get faster: reading state off query results no longer copies the whole state bag per object per tick, state writes do half the work they used to, and Savi's live edits over big worlds respond quicker.
  • Savi working in the background no longer takes over the area you're playing in. Her behind-the-scenes builder session used to be able to win control of the exact arena you were standing in — and it can't run physics, so bots froze, pickups never appeared, and "nothing works" until a restart. Now the player standing in a place always holds it; Savi's session only runs the places nobody is in.
  • Terrain works everywhere in multiplayer worlds — no more falling through far areas. Ground far from the players — under a driverless car, a wandering NPC, anything physics moves — is now guaranteed solid in multiplayer rooms, exactly like singleplayer. Before, objects in far regions of a shared world could fall through or let players walk through terrain that never got its collision built.
  • Placeholder images keep their shape while models generate — a tall character concept no longer renders squashed and stretched on its loading card.
  • The touch stick on phones no longer freezes and vanishes when you move your thumb slightly. In games with on-screen UI, the browser could steal a touch a few pixels into a drag (it read the drag as a page-scroll attempt), which killed the movement stick, camera drags, and hand-rolled touch pads mid-gesture. Touches on your game now belong to your game from press to release — and your scrollable panels and lists still scroll exactly as before.
  • Savi can rename something across your whole game in one step — and show you every change before making it. Renaming a town, an item, a character used to mean editing every script that mentions it, one file at a time. Now one call previews every affected line across every script, and the same call applies it.
  • Replace mode now does what its name says: painting in Replace pushes out the world's natural ground as well as other painted materials — a full-strength stroke leaves pure paint instead of a half-and-half blend. Erase still returns everything to natural, undo still takes the whole stroke back.
  • Ground material transitions render crisper everywhere: where two materials meet you see the dominant one with organic speckle instead of a translucent wash.
  • Games with lots of moving objects spend less time in memory cleanup — motion updates now recycle their bookkeeping instead of generating garbage every tick.
  • Editing large worlds is faster: applying a change to a world full of roads, power lines, fences, and rooms no longer re-derives every generated piece on every edit — an edit now costs work proportional to what actually changed (~4x faster applies on lowering-heavy worlds, and burst edits like big builder-script runs spend far less time in bookkeeping between steps).
  • Games with several AI-generated or uploaded 3D models hold dramatically less memory on phones — the engine now frees each model file's raw bytes once its textures reach the GPU instead of keeping whole files around, which is a big step against the "game reloads on iPhone" class of crash. Busy building sessions also idle lighter: landing model metadata no longer re-scans all your scripts, and big boot bursts no longer leave permanently enlarged buffers behind.
  • Long sessions in sound- and media-heavy games use much less memory: decoded sound effects, warmed video clips, and edited-away material shaders are now released when nothing is using them (they reload on demand), which especially helps phones stay alive in big games.
  • Games with lots of timers — every() loops, staged builds, scheduled waves — no longer pay a per-tick sweep over every armed timer, and the engine's own tick loop allocates far less, so long sessions hitch less from garbage-collection pauses.
  • Invisible rotated walls: static boxes spawned with a yaw could, around busy spawn bursts and reconnect churn, briefly (or in bad cases permanently) collide as if unrotated — cars slamming into walls that measurably weren't there. Static colliders now always converge to their authored rotation as soon as it arrives, no matter what order the network delivered the pieces in.
  • Terrain texture tiling now uses the same textureScale meaning as objects: meters each texture tile covers. If your world's ground suddenly looks ultra-dense, ask Savi to convert the old values (new = 2.5 / old).
  • Your character no longer keeps running on its own after opening menus mid-stride. If you were holding a movement key when the chat or an overlay opened, the release landed on the menu and the game never saw it — the character kept walking with no key down until a god-mode round trip cured it. Held keys now release the moment the game surface stops receiving your keys.
  • Mouse aim no longer goes dead after quickly closing chat — the game re-grabs the mouse itself. Before, flipping the chat overlay open and shut at the wrong instant could leave a first-person game with working keys but a dead mouse until you opened and closed chat again.
  • Your mouse's side buttons can now be bound — quick-melee, ping, whatever your game wants. Ask for mouse4 / mouse5 binds; the browser's back/forward navigation stays out of the way while a side button is bound.
›technical notes
  • The singleplayer save destroy/respawn storm is gone: one spec-apply domain per world (dig 28aafd23, row 2445; engine-bug dump db83ebcd). The singleplayer glue mounts BOTH spec-apply lanes on the one authority world — tome/spec-sync (client, netIngest 20) and tome/spec-sync-server (simulation −950) dedup by name only. Per save (revision bump, zero content delta) the client lane applied a filterSpecForPlaced doc over a world that is the simulation authority for ALL places: every non-kept authored place's objects emptied, place instances expanded over the shrunken view to ~0 objects, and the reconcile diff destroyed the difference (302 destroyed in the dump — onDestroy hooks fired, spawn-owned children cascaded), then the server lane's whole-doc apply respawned it all (307) the same tick. The fix: when the doc being applied resolves singleplayer, the client lane applies the WHOLE merged doc and removes TomeUnfilteredSpecResource (its absence is the established "not place-filtered" contract — presence also keyed the effective-places expansion into emptying non-kept INSTANCE places, and spec-update/reset into re-filtering). Both lanes are now content-identical, so applySpec's delta-0 gate short-circuits whichever runs second — the population is written once and never fought over. Place travel on a singleplayer world stops tearing down the place left behind for the same reason (the authority world holds every place; rendering already place-scopes via the renderer's PlaceMembership include-filter). Multiplayer clients are byte-identical: the filter, the unfiltered record, and every downstream consumer keep their exact shapes.

  • The interactive grammar reads script-wired controls, and the touch arbiter yields over every authored interactive surface (row 2437 — DJ's affected games; dumps 3238173e, 07242235, 18bbbfa7). The defect class: a pure-DOM control wired by script (el.onclick = fn, el.onpointerdown = fn) never matches the attribute-based interactive-opt grammar (INTERACTIVE_SELECTORS), so it never gets the pointer-events grant — its presses fall through to <body>, forward with overUi:false, and the host touch arbiter's geometric claim bands take the touch (stick spawn, look drag, world press) out from under the visible control; it presents as z-order eating taps, and inputs.touch.enabled:false "fixes" it only by killing the arbiter's claims. Five legs: (1) the grammar widens — isAuthoredInteractiveElement reads handler PROPERTIES (onclick/ondblclick/onpointerdown/onpointerup/onmousedown/onmouseup/ontouchstart/ontouchend), inline cursor:pointer, and inline self-opts (pointer-events:auto) beside the attribute selectors, and INTERACTIVE_SELECTORS gains the missing [ondblclick]/[onpointerup]/[ontouchend] attribute forms; (2) the DOWN verdict widens geometrically — a fall-through press whose point sits on a visible authored interactive control (findAuthoredControlAt) forwards overUi:true, so the arbiter yields (onCanvas: !pass.overUi) while bare-canvas presses keep overUi:false (movement sticks still win empty space); (3) the tap machine adopts fall-through gestures — the activation fires on the control (synthetic click at finger-up, iOS pointercancel verdict included) and the element heals (makeElementInteractive) so later gestures ride the native path, plus a 500ms grant cadence over the frame body heals controls wired after the morph walk (property assignment fires no mutation event); (4) the tap-rescue lane widens from the sendAction corpus to every authored interactive element — pure-DOM onclick toggles get the same guaranteed in-budget delivery; (5) lifecycle game mounts carry the data-input-capture="ignore" UI-plane marker the render container always had, so a mounted element that opted itself in reads overUi:true instead of being double-delivered (control AND engine stick). makeElementInteractive now preserves authored inline touch-action (a hand-rolled stick's touch-action:none is intent).

  • The relay ingress-age self-heal no longer misfires on the parked-drain trickle (dig 81ce11e4, row 2434) — parked time is not staleness, and the predicate gets a depth floor. Fix 2364's heal fired on age alone at the drain edge, but the ingress drain runs on the sim frame loop, which PARKS while the tab is hidden (shouldSelfScheduleWorkerRuntimeFrames) — so every ≥30s tab-hide over a dev room's background STATE trickle (~0.3–0.7 rows/s; the misfire ring: 40 heals in one creator's evening, 16–438 rows across 31s–1300s hides) read as a wedged drain and bought the lane's most expensive remedy: close 4439, the "Connection lost" overlay at the exact tab-return instant, and a resume=0 keyed-heads refold of a multi-MB spec to avoid draining a few hundred rows. Two guards, one per false class: (1) the visibility-resume edge re-stamps every queued relay.peer.frame row's arrivedAtMs (rebaseRelayIngressArrivalsOnResume — the staleness clock measures a RUNNING drain that failed to consume, so it restarts when consumption becomes possible; a drain genuinely wedged AFTER resume re-ages past the bound and still heals), and (2) the heal now requires material depth beside age — RELAY_INGRESS_STALE_REPLAY_MIN_ROWS = 2048, one relay journal self-compaction window (JOURNAL_SELF_COMPACT_ROWS), because a replay deeper than that is history the relay itself would have folded away for a fresh joiner while anything shallower drains in one gulp for less than the refold costs. Depth is a floor, never a trigger: a deep-but-young queue of distinct keys is honest load and drains normally (tide's fix-2364 law, kept). The true-fire class is untouched — fix 2364's motivating specimen (11.7k rows aged past 30s) heals exactly as before.

  • The self-addressed rail never forwards, and the relay-wire stamp is tri-state honest (pin 04da3c48, row 2430 — enfeul's every-join breakage). On a relay wire whose welcome-edge stamp (ClientRelayWireModeResource) read stale-FALSE, enterPlace took the kernel forward branch for the client's OWN avatar — but the relay excludes a sender from its own fan-out and the traveler's only simulator IS the sender, so the forwarded rail.enterPlace had zero appliers anywhere: id returned, membership never moved, structurally undeliverable. Three legs: (1) STRUCTURAL — the enterPlace relay fork now reads call-time wire truth (clientRelayWireTruth: the transport's live wireMode(), stamp fallback for reader-less worlds and the sniff hold) and widens self-apply to the receiving applier gate's own predicate (isClientAuthLocallySimulatedEntity ∪ the existing forward-gate admissions ∪ god-parked), so a traveler this seat would apply for is never forwarded; (2) THE STAMP — applyWelcome no longer writes wireMode()==="relay" as FALSE off an "undetermined" hold (first-frame sniff, between-sockets churn): an undetermined welcome PARKS the whole tier edge (stamp + uploader baseline adopt + world-sync arm) and the ingress drain completes it the tick the sniff resolves; a genuine relay↔kernel flip logs one rate-bounded line; (3) VISIBILITY — dump capsules now carry a wire stamp (live transport wireMode + welcomeStamp) via the netcode.wire worker debug door, so this class never needs a 13.5MB capsule dig again. E2E twin: relay join with the welcome applied inside the undetermined hold (the fresh-redial + backoff churn shape) → own-avatar enterPlace(createIfMissing) → membership moves and the minted instance expands on every fold.

  • playSound gains follow: true (row 2037, dig 55122dec). A positioned one-shot that names it mirrors its SOURCE entity's WorldFeetPosition while the voice lives — the primitive-not-pipeline fix for self-emitted movement sounds (installation-03's footsteps: spatial plays pinned at boot-plant positions trail opposite the mover's velocity for the life of every clip; bystanders hear correct physics, the mover hears their own boots behind them). Mechanism is the one loops already use: audio/prep re-reads emitter positions per frame, so the fix is a position mirror on the pooled carrier — spawnPlaySound records follow + the source entity in the one-shot registry (OneShotSoundEntitiesResource), and a new client system (tome/one-shot-sound-follow, renderPrep −110, ahead of audio/prep at −100) copies source→carrier each frame, making a follow voice exactly as fresh as an audio: loop on the mover itself. A source that despawns mid-clip stops mirroring and the voice finishes at its last position (one-shots outlive their emitter, by design). Version boundary pinned red-first: flag absent = byte-identical wire and behavior (unset one-shots stay world-pinned); the option rides the wire only on positioned non-loop plays that named true (unpositioned follow teaches once and lowers — non-spatial plays are already ear-locked; loops lower silently — tracking loops are the audio: component's lane). Pins: tome/__tests__/playsound-follow.test.ts (wire shape + client mirror, both halves).

  • Audio listener basis prefers the renderer's displayed camera (row 2036, dig 55122dec). readListenerTransform/readCameraTransform (engine/audio/prep.ts) now build the listener pose from viewState.rendererTransform (pos + rot) when renderer feedback exists, falling back to the script-authored viewState, then the entity transform — the exact preference the aim basis has used since camera-derived-axes' resolveViewState. Closes the latent audible-camera ≠ visible-camera seam: undeclared mouse-orbit rigs discard authored rot for display, so a camera script with missing or convention-flipped rotation writes produced wrong/mirrored audio with flawless visuals and correct aim, silently. Explicit AudioListener.forward overrides (the 2D fixed-basis law) still beat every camera-derived basis. Pinned red-first in engine/audio/__tests__/prep.test.ts (renderer-preferred, authored fallback, explicit-forward precedence, DrawCamera fallback path).

  • Savi surfaces: follow in the playSound/playSoundAt option types + @tomeapi doc (api-reference regen), and the audio skill's world-sound section names the trap and both exits (follow for self-feedback, or non-spatial).

  • AudioParam writes now go through a dedupe + suspend gate at the renderer seam (Safari P0, #w-rendering 2026-08-01). Every AudioParam write — including plain .value = — inserts a WebKit timeline event (AudioParamTimeline::insertEvent, one heap allocation each); prep re-sends full state every frame (≥16 inserts/frame with zero voices, ~6 more per voice), and while the destination is autoplay-suspended nothing consumes the timeline — measured ~12 MB/s WebContent growth until the machine swaps to death. Now a write whose target equals the last value actually written is skipped (per-param last-written cache; epsilon for ramp targets), and while ctx.state !== "running" no per-frame param write lands at all — desired state parks and the statechange back to running flushes one converging refresh. Ramps still schedule normally while running (no zipper-crackle regression); bounded one-shot writes (node creation, cull fades, duck ramps, vibe edits) stay ungated. Pinned by renderer-param-timeline.test.ts (red on the old renderer, 9/9 green now).

  • The erase brush clears EVERY painted material at once (jacob's punch list, #inventors 1785628548). Erase on a material brush was a sign flip on the armed field only — erasing "dirt" left sand and grass paint standing, so returning ground to its natural look took one erase pass per palette material. An erase dab (Alt, the Mode chip, or negative-strength armBrush) now runs the paint script over every authored terrain:material:* field in the place — enumerated from the fields that actually hold paint, so orphaned fields whose material left the palette clear too. Material fields ONLY: marks, terrain:height, decor:*, and the generator's natural weights are untouched — erase returns the texel to its natural generated look. The expansion is per-dab and never persisted (brush.fields stays the armed field); commit/undo bookkeeping now iterates the stroke's touched-field ledger, so one undo restores every field the erase cleared.

  • Replace mode: paint that wins. New third Mode on material brushes (chip: Paint / Replace / Erase; session flag replace; armBrush({ replace: true })). A replace dab paints the armed field exactly like additive paint (max(current, amount), amount = falloff × strength) and additionally scales every sibling painted material field by (1 − min(1, amount)) — engine math in the session, so the paint-script contract stays one-field and overridden material scripts inherit the mode. All siblings scale by the same factor (their relative mix is preserved as they fade); at strength 1 a single core dab zeroes them. Natural generator weights still compose underneath (chunk-build's additive law is unchanged), so strength remains the lever against natural coverage: replace-at-2 visibly dominates. Default stays additive — jacob ruled additive correct; replace is the opt-in.

  • Cursor label gains "Replacing"; the Mode chip label gains the same. Non-material brushes keep the two-option chip (no sibling family to lower).

  • Pinned in brush-material-erase-replace.test.ts: erase clears all painted material fields (armed-or-not) and leaves height/decor alone, emptied fields drop from the spec, one undo restores them all; replace scales siblings proportionally (1.0/0.5 → 0.5/0.25 at strength 0.5) and compounds across dabs; Alt-erase beats an armed replace; additive-without-the-option writes no sibling chunks at all; the chip index contract; the armBrush surface.

  • Engine diagnostics adopt jacob's DM taxonomy (#inventors 1785635891): everything in logs; errors and storms ping. Every client-reported diagnostic still lands in getLogs() the moment it arrives (unchanged). Each of the 98 rail codes in engine-diagnostics.ts now carries a class: error (broken/wedged/terminal — script compile/runtime faults, renderer crashes, exhausted self-heals, physics corpses, engine parks of authored content) DMs Savi once per episode on first occurrence, with repeats counted to the log; warning (renders degraded-but-playable, placeholder shows, self-heals, teaching, environmental — texture/model load failures, missing clips, slot exhaustions, quota parks, schema skew) never DMs per instance. Either class escalates ONE storm DM when a single code accumulates ≥25 reports inside a 5-minute window ("N reports of this kind… something systemic; getLogs has every instance"). The old trailing "+N more" rollup flush — one hidden Savi model turn per active code per window — is deleted, along with its timer machinery and budget-carry branches; the perf-pointer lane (#235 hourly categories) and the room-global 5-DM/min budget are untouched. Class table pinned in engine-diagnostics.test.ts.

  • Query iteration SoA component VALUES are flyweights (ruled 2026-08-04, squeeze hunt ECS core). The engine-internal QueryResult iterator contract narrows: soa3f32/soa4f32 columns (WorldFeetPosition, BodyPosition, WorldScale, WorldRotation, LocalFeetPosition, LocalRotation, LocalScale) now hand out ONE reused value object per column per QueryResult, refilled per row inside fillRow — the last per-row mint #12093 left standing (one {x,y,z} per SoA row per pass; a 20k-row soa3 query paid 20k mints per iteration, now 1 per column per QueryResult). toArray() keeps fresh mints per row and stays the retain-safe form. Audit receipts (bound: every .query( call site across apps/cf-kernel/src including tests and benches; positive control: the known SoA-tuple site tome/api/query-utils.ts:554 and the flyweight row-identity pins were both hit): exactly one non-test SoA-tuple iteration exists (query-utils.ts emitFullScanMatches — consumes in-loop, scalar emit, no retention), all test SoA queries use toArray(), no import aliases of the seven SoA components, no multiline query calls, no generic wrapper passes SoA tokens into world.query. Zero retainers found, so no consumer migration was needed. The script-facing boundary stays fresh-mint by construction and is now named in the QueryResult doc: api.query builds its own results (query-utils.ts buildQueryResult captures scalars and lazily mints an escaping Vec3), the exec overlay's query builds its own rows from staged values, and world.get mints per call — the flyweight never crosses the ECS layer. This change is invisible to behavior scripts and run_script.

  • Pin flip (documented per the ruling's duties): the retain-safety pin was documentary — the QueryResult interface doc ("SoA columns a fresh object per row", world.ts) plus a comment in determinism-pins.test.ts; no executable assertion pinned value freshness. Both texts now state the flyweight contract, and a NEW executable pin ("SoA column values are per-column flyweights during iteration; toArray mints fresh values", determinism-pins.test.ts) asserts one reused value object per column with correct per-row contents, and fresh distinct objects from toArray().

  • drainEventAdds returns a shared frozen empty array when nothing fired (the common case for its ~11 per-tick/per-frame drain sites — juice ×3, sound-loop ×3, purchase prompt, renderer particle burst, audio prep/deriver: ~400-800 throwaway arrays/s per session at rest, now 0), and its declared return type is readonly StickyEventAdd[] so tsc enforces the read-only caller contract mechanically (all 7 production call sites audited read-only; a caller pushing onto an empty drain now throws in strict mode instead of silently cross-contaminating drains). sound-loop-ingest processes its three juice drains in place instead of spread-concatenating them into a fourth array per server tick.

  • Replication drains refill a persistent changedComponentIds scratch in place instead of minting Array.from(set).sort(freshClosure) per drain — both the ECS-level drain (engine/ecs/replication.ts) and its production twin (runtime/replication/room-replication.ts, one drain per AOI-bucket class per egress tick). Consistent with the delta-is-reused-scratch contract already documented in replication-delta.ts; consumers (room-wire-codec frame encode) read synchronously within the same frame build.

  • behavior-update's per-tick debug readout (TomeBehaviorUpdateDebugResource) mutates one engine-owned resource object in place (ensure-once + six field assignments at all four system exit paths) instead of minting a fresh object literal per tick per world. Reader census: the sim-probe markers handler (structured-cloned at post time), tests, and the behavior bench all re-read the resource per observation; none diff object identity across ticks; behavior-update never runs inside an exec overlay.

  • New bench coverage for the two blind spots the sweep named: ecs/query gains query/1-soa3 (an soa3f32 population — the storage class every transform/pose query actually iterates; the old fresh-mint path measured 283µs/pass vs 254µs flyweight at 20k rows, mint kill 20k→1 per pass) and a churn/commit+churn/query pair (one spawn+despawn per iteration invalidates every cached query shape; measured ~2ms/churny-iteration of snapshot-rebuild tax at 20k entities across 3 shapes — the receipt for the still-open query-snapshot-rebuild-under-churn row, which this PR deliberately does NOT implement: incrementally-maintained snapshots need lend-bit copy-on-write across every store×shape pair plus a mass-churn fallback to hold snapshot identity byte-identical, a design project with engine-wide blast radius, skipped honestly per its own P verdict).

  • The game document can no longer paint a scrollbar (tiger, #inventors 1785641498). The engine page's html/body had no overflow guard, so any in-flow element that outgrew 100svh by a few px — in tiger's session, a transient during an art cook, frame-coincident with the kiln "Spawning art" chip — painted a full-height document scrollbar over the game's right edge on always-visible-scrollbar setups, and window.scrollTo could slide the whole canvas (measured: 8px in-flow overflow → scrollHeight 892 in an 884px viewport, scrollTop movable). Root overflow: clip now propagates to the viewport as hidden (CSS Overflow §3.3): no document scrollbar, ever, regardless of what transient DOM appears. In-game scroll surfaces are untouched — realm UI already scrolls inside its own overflow: hidden chrome-host container, and the kiln page's own scrollers (chat, settings) live in the parent document. Pinned by game-document-overflow.test.ts.

  • The stalled-spec-lane HUD freeze dies at the sample boundary (row 2186 — pinball 65cc2257/4af4094d, Dayspring aa6bb90d). The game HUD's compile slot now keys on CONTENT, not mount time: CompiledSpec.uiSource records the exact source ui.render compiled from, and the HUD receipts' compile breadcrumb carries sourceHash (the live doc's resolved render-source hash), so compile.sinceMs re-stamps when the authored content moves — "pinned at boot" can no longer read identically for a healthy lane and a wedged one. When the live doc's ui source diverges from the compiled fn's content key for a sustained window (~3s at 30Hz, the same bar as the compile-missing confession — far past any legitimate apply→compile gap), the controller recompiles the render from the live doc through the same content-keyed cache a real apply uses (a later apply of that content is a cache hit on the healed fn), swaps it into the serving graph in place, and confesses once on the fault rail naming the stalled apply lane. A source that fails to compile keeps the old render serving, reports through the ordinary compile reporter, and re-arms on the next edit. Field signature this ends: compile ok/sinceMs pinned at boot across ui.js v696→v708 while sibling edits applied, sent frozen, deduped past 88k, restart-rooms as the only cure.

  • The input-binding validator stops billing Savi for entries the platform wrote (r-20a666a8, ravi's 08-01 playtest dig 3c3d2509). Every 3D game minted from the starters carries lookX/lookY/aimYawSin/aimYawCos/aimPitchN in inputs.axes — the starter template ships them — and the #11518 validator flagged all five as "engine-internal axis … Remove this entry" on the merged map, so EVERY patchInputs (including ones that never touched axes, including writes that later rolled back) drew 5 warning lines for entries Savi never wrote. During the 08-01 playtest the noise drowned the real schema errors in a 19-second terrain thrash. The condition was both provenance-blind and factually wrong for two of the five names.

  • lookX/lookY leave the scold entirely — they were never "engine-internal" in the claimed sense. They are binding-fed axes with an engine DEFAULT (buildDefaultBindings injects the mouse binding only when undeclared — the hasOwnProperty guard exists precisely so declarations override, and gamepad-look treats explicit rightStick → lookX/lookY bindings as the look gesture). Declarations now validate like any axis: a remap is silent, a junk source names itself, and an EMPTY lookX: {} — the one genuinely hazardous shape, since it suppresses the default and binds nothing — gets an honest teach ("replaces the engine's default mouse-look binding with nothing … remove the entry to restore the default") instead of a false one.

  • Engine-computed axes (aim*, cam*, pointer*, touchActive) warn only on the genuinely-wrong shape: a declared hardware source. input-resolution seeds each axis from the engine's setAxis write, then keyboard bindings override it and mouse/gamepad deltas add to it — so aimYawSin: { keys: "q/e" } really does fight the camera-derived value, and still warns (with the true mechanism named). A sourceless {} declaration compiles to nothing, blocks nothing, and is exactly what the platform's own templates planted — it is now silent everywhere (patchInputs rail and spec-sync console lane both ride collectInputBindingFaults).

  • Multiplicity: one line per patch, never one per axis. All sourced engine-computed axes in a map collapse into a single fault line naming them together — the 5× per-write fanout shape is structurally gone.

  • The planting stops (structural elimination). The five declarations are deleted from packages/game-starters/starter-3d.ts, cf-kernel's DEFAULT_GAME_SPEC, and kiln's DEFAULT_ADVENTURE_GAME_SPEC (behavior-identical: the engine injects the same mouse-look binding when undeclared, and the aim basis never read declarations at all). The teaching corpus stops teaching the shape: the camera skills' "Required Inputs" JSON and the @tomeapi-example camera SPEC-REQUIREMENTS lines now say these axes are engine-provided reads, declared only to remap look. The starter drift fixture is regenerated in this commit.

  • BREAKING: query results and object summaries hand out the live tracked state proxy (ruled 2026-08-04, supersedes #12092's detached snapshot). buildQueryResult and getObjectSummary (tome/api/query-utils.ts) return .state from the shared per-(world, bag) proxy cache in the new tome/api/tracked-state.ts — the exact object getObject()/getState() return. Reads are 0-alloc off the stored bag (the per-read {...state} spread + read-tracking wrap chain is deleted); writes mark the component updated, replicate, and ride the client-auth intent rail; a retained result observes live state. getMergedTomeState/getMergedTomeStateByIndex (state-utils.ts) and trackClientAuthSnapshotState (intent-context.ts, the detached cross-writer snapshot wrapper + its per-call WeakMap/JSON.stringify path cache) are deleted — the tracked proxy's own traps record reads/keys/has-probes into the ACTIVE invocation at trap time (same dynamic attribution, plus the toJSON-probe basis exclusion the snapshot tracker had).

  • BREAKING at one seam: native structuredClone of a result's .state throws (proxies are not structured-cloneable). Scripts are unaffected — their structuredClone is the unwrapping bridge (script-value-clone.ts; every tracked proxy mint now registers in its proxy registry). The exec result boundary (exec/engine.ts finishOutcome) unwraps registered proxies before the reply crosses, so return api.query(...)/return api.getObject(id) from run_script now arrive as plain data (getObject views previously DEGRADED into a serialization note — fixed by the same unwrap). api.job args gain the same membrane unwrap as the property-write lanes.

  • Tracked-proxy write path (row: tracked-proxy-write-double-materialization): the set trap stores to the TARGET directly and fires the recorder itself, instead of receiver-routed Reflect.set re-entering the defineProperty trap — one quantize walk, one markComponentUpdated, zero descriptor mints per write (was 2 traps + 2 quantizes + 2 marks; object-valued writes paid the whole container copy twice). Recorder emission stays the single funnel, after the local apply. Bench (this box, micro): getState mutate top-level 321→170ns, nested 319→163ns; object-valued assign 277ns single-copy.

  • patchState/patchObjectState/deleteState (row: patchstate-triple-whole-bag-walk): an O(patch) write verdict (statePatchChangesStoredState, the deepMergeState mirror under the column write's own fastDeepEquals law) runs BEFORE the merge — a changes-nothing patch skips the {...current} merge copy, the full-tree sanitize walk, and the full-width equals walk (cross-writer intent emission and the ledger-1157 durable-mirror suppression contract are preserved verbatim; the specValuesEqual persist verdict stays its own law). Changed patches take the new trusted pre-sanitized lane — world.setResolvedByIndexPresanitized / setObjectMapPatch(..., presanitizedChanged) — which skips sanitize (merged bag is wire-safe by induction: sanitized stored bag + membrane-quantized patch values) and the equals gate (verdict already proved the write lands). Raw engine world.set callers keep the full sanitize net. Bench: patchState 2-key vs 50-key bag 1053ns, no-op 193ns, 2-key vs 100-key nested bag 2267ns (new cases).

  • getObjectSummary (row: get-object-summary-eager-materialization) rebuilt on the lazy-accessor literal: id eager; tags/feetPosition/state memoizing accessors — contact/interact/voxel payloads stop paying the whole-state spread + tags copy + position mint when the hook reads only other.id, and land on the SAME final shape as query results. The missing-position {0,0,0} default still skips the physics-quantize check (hadPos capture); present positions keep the read-time pose-driven check.

  • Script-transaction overlay (row: overlay-entity-walk-per-query): currentEntityIds is memoized against the overlay's own membership funnel (spawnEntity/despawn — the base world is frozen for the overlay's lifetime), the O(spawned×N) includes scan is gone, and query-row iteration memoizes on a staged-write epoch (bumped by every setComponentPatch and membership mutation; rows embed references, so in-place read-clone mutation needs no edge). Bench (new case, no pre-change twin): 50 staged spawns + 50 entity enumerations over 3k entities = 272µs total (~5µs per enumeration); the deleted term was O(N + spawned×N + N log N) per enumeration — ≥ 150k includes compares + a 3k sort each, at this shape.

  • Flipped pins, each one the ruling's own semantics: query-lazy-results.test.ts "state stays a detached snapshot: result writes never reach the world" → "state is LIVE: result writes land"; its native-structuredClone parity pin → script-bridge clone + native-throw pin; new pins for door identity (query result / summary / tracked door alias one proxy) and live retention. client-auth-intents.test.ts rewired from the deleted getMergedTomeState door to getObjectSummary(...).state (all basis-recording expectations unchanged — get/has/ownKeys/JSON recording now comes from the tracked proxy's traps); its native structuredClone-of-snapshot assertions flipped to the script bridge. query-fast-path.test.ts reference impl reads the tracked door. New exec pin: returned query results/views cross the wire as plain data (clone-fallback.test.ts).

  • Behavior-preserving invariants pinned by the untouched suites: client-auth e2e + intents (basis order, inc/set classification, revert inverses), script-transaction overlay + tx-spatial query parity, exec snapshot withholding drift pin, determinism pins. Caches name their invalidation edges at the definition site (tracked-state.ts header; overlay memo comments).

  • An in-place player always outranks the standing (headless builder) session in the per-place host election (ledger 2464, incident f466014b — Blob Wars). A room_host-class session never ranks at resident tier: its body residence folds as one more body-independent claim (relay-place-elect claimAnchor), so any eligible real client standing in a place takes that place's seat — in the v5 fold, the room-truth derived election, and the fold's honoring gate for relay h:* assignments (a relay predating the rule can still journal a shell seat; the fold degrades to the derived election and seats the resident player). cf-relay's own seat brain carries the same rule: host-session:* candidates rank behind every real client in pickWinner, and a SITTING shell yields at the first qualifying real-client beat — the one carve-out from first-eligible-never-migrate besides graded health. The shell still hosts every place that is empty of players — that is its job, and the fail-closed floor (weak/cooling/shell beats hostless) is untouched. Before this, the shell — present whenever Savi runs world probes — could win a place a live player stood in and host it with no physics runtime and no terrain definition: raycasts answered null, placements parked, bots embedded in the floor, while the player's own sim-health read green.

  • restart-rooms verdict honesty on client-hosted rooms (the same incident's rider). A relay world reset that WENT OUT with only its receipt missing now answers the structured verdict reset_sent_unconfirmed — never degraded — and the message says what was done: the reset was sent to the client-hosted room, there are no server rooms to restart by design (architecture, not failure). The field reset provably landed (journal + heads cleared, host re-elected) while the old verdict read 'degraded / no server-hosted rooms', teaching Savi her one remedy fails.

  • The physics-anchor chunk-residency guarantee lives with the collider-assembling pass (ledger 2373 re-land of #13063). LOD0 chunks now mint under every dynamic body the local realm simulates — awake vehicles unconditionally with velocity lookahead — in every world class that assembles colliders: singleplayer, multiplayer client-auth clients (the class that was dead), zero-player rooms (spawn-fallback streaming), and seat-holding container-tier servers. The guarantee was keyed on isSoleClientStreamerWorld (client && singleplayer), so no pass ever minted the chunk under a far body in a client-auth room and the chunk-rescue fought each ballistic re-fall forever (dig 525e1a23: 1,942 fires in one session). The stationary-player coords fast path and the server idle-skip signature fold in the anchor CELLS, so a vehicle crossing a chunk boundary under a stationary player busts the cache while an anchor moving within its chunk keeps it. Anchors are owner-scoped in multiplayer (the physics step's own follower boundary), so N clients never pay N× chunk builds under each other's followers.

  • What the revert demanded, cured structurally: #13063 was reverted (2ef614ede4) because its static streaming.ts → @/tome/client-auth import closed a cycle through client/netcode (streaming.ts is inside netcode's own graph via netcode-transport → relay-peer-apply → edit-wire → tilemap), breaking vi.mock module identity — KernelTests shard-2/3 red on master (client-auth-intent-deferral, relay-terrain-edit-lane; the ledger-752 class recorded on netcode.ts's relay-tome-consumers seam). The re-land injects the server ownership predicate through a leaf seam (physics-anchor-scope.ts) wired at module scope by terrain-systems-shared.ts, which already legally imports client-auth; streaming.ts gains no tome/client-auth edge. Both formerly-red suites are green with the fix applied (51/51).

  • Held seats gate the server pass, never the declaration (codex P1 on #13063): includePhysicsAnchors and the idle-skip signature's anchor gather key on hasAnyServerHostedPlace, not isServerPlaceHostWorld — a declared place-host world whose seats are all client-held has a dormant sim stack and pays no per-tick body traversal for anchors it never owes.

  • chunk_rescue_remediation_failed with chunkEntity=missing is a RESIDENCY verdict ("streaming never reached here") with a structured verdict field, instead of blaming rebuild requests that were never submitted.

  • Placeholder concept images keep their aspect (tiger's squashed shinobi, dump ad7dcd12). The mcdn placeholder ladder's image rung stretched the decoded preview across the full square hologram quad (packPlaceholders packs one scalar drawn size; UV spanned the whole quad), so a 1024×1536 portrait humanoid card rendered 1.5× too wide. packPlaceholders now packs a contain-fit rect (placeholderPreviewFit — the fitted rect as a per-axis fraction of the quad, computed from the decoded texture's own dims) into the instance's two free pad floats, and the preview shader letterboxes the image to that rect — centered horizontally, bottom edge at the entity's feet, fragments in the letterbox margins discarded exactly like the BiRefNet-cut alpha. Quad size, pick volumes, crossfade, and TAA velocity behavior are unchanged; procedural/failed instances keep byte-identical instance data.

  • World touches in the authored realm survive the browser's pan-slop — the mobile touch stick stops vanishing on slight movements (incident 300a3b65, Savi-filed; enigmatic1's report, 08-01). When a game has live authored UI, the realm frame iframe is the viewport-wide hit target for every touch, and a fall-through press (target <body>/<html> — the pointer-events law) is the world's: the platform stick, look-drag, and world gestures. The frame document shipped with no touch-action posture, so the browser's touch gesture disambiguator was free to claim those presses for a pan at its ~10px slop — pointerdown → pointercancel, no pointerup — and Chrome cancels even when nothing in the chain can actually scroll (browser-gate receipt: non-scrollable host, non-scrollable frame, micro-drag still cancelled). The arbiter then correctly ends the analog claim, so the stick died ~10px into every drag — forever under its own 8.16px deadzone, which is exactly Savi's filed observable (touchActive:1 with moveX:0/moveZ:0 while the player holds the stick, "the stick widget vanishes when pressed or nudged slightly"). The shell now pins touch-action: none + overscroll-behavior: none on the frame's html/body: a world press can never start a browser pan, so its pointer stream stays the platform's from down to up. Same law, third layer — the canvas has carried it since day one (browser-host/dom.ts), kiln's game routes carry it (game.css), and the frame the UI actually lands touches on now does too.

  • Pinned by tests/browser/suites/world-touch-stick.pw.ts (real shell + real frame bundle, CDP touch): the micro-drag world press delivers down → moves → up with zero pointercancel under a scrollable ancestor chain AND when the frame's own root scroller has extent (body touches pan neither); mobile-authored-ui-actions.pw.ts's scroll-stays-scroll matrix pins that authored scrollers still really scroll (CDP flick moves #shop-list). Arbiter-level law pinned in arbiter.test.ts: an analog claim has no slop-cancel — sub-slop micro-moves plus a hold past every gesture window keep the claim alive to the lift.

  • api.replaceAcrossScripts(search, replacement, opts?) — the bulk-rename verb (r-a6e3cbe4, derrf's town rename). Multi-file find-and-replace across every game script: literal text by default; opts { regex?, caseSensitive? (default true), includeScripts?/excludeScripts? (glob or exact-path list), dryRun? }. dryRun: true returns the full report — {script, line, before, after} rows plus per-file match counts — with ZERO writes; the wet run returns the same report with applied: true. Each changed script stages through the existing setScript lane (one spec swap + one recorded setScript mutation per file), so persistence, forwarding, and undo ride machinery that already exists; the report carries a per-file verdict instead of promising all-or-nothing. Zero matches is an empty report, never an error. memory/ and skills/ keys are never candidates, so a sweep cannot even attempt the writes those lanes reject.

  • Replace mode fully replaces — natural ground included (jacob's 50/50 crop, #inventors 1785628548). Replace previously zeroed painted siblings only; the generator's natural weights still composed underneath, so replace-at-1 over natural grass landed at 50/50 — "it's NOT winning it's merging". Natural coverage is derived (materialAt re-evaluates per texel), so there was nothing stored to write down; the suppression now persists in its own channel, terrain:natural-mask (0–1). A replace dab raises it to amount (max-composed, clamped to 1) alongside the sibling scale; chunk-build scales the generator's material result by (1 − mask) BEFORE marks and paints compose — a full-strength core is pure painted material. An erase dab writes the mask back down (subtract, like the material script's signed erase), so one erase gesture returns replaced ground to its natural look and the emptied mask drops from the spec. The mask rides the stroke's touched-field ledger: one undo restores paint, siblings, and suppression together. Marks stay outside the mask — they're authored decoration, the same boundary erase draws.

  • The mask is an ordinary field: readField/writeField see it (documented in the ObjectAPI field docs), the scatter material matcher applies it (ground the renderer shows as pure painted material reads as that material for material-conditioned beds), and the terrain inputs hash carries it name-keyed beside the painted material fields.

  • Texture transitions sharpened (the blendWidth follow-up #11801 deferred). The pbr weight resolve's crossfade window was floored at TERRAIN_HEIGHT_BLEND_DEPTH (0.18) — as wide as the ±0.18 texture-height bias that decides which layer wins each texel, so across a whole transition band the losing layer rendered THROUGH the winner: 2:1 and 50/50 paint ratios both read as a translucent ghost-merge. The window floor is now its own constant, TERRAIN_BLEND_SHARP_WIDTH = 0.06 — the height bias still carves which texels each material wins (grass blades between stones), but the loser fades out within a texel-scale band: dominant paint reads as dominant-with-speckle, partial rims dither instead of washing. Non-pbr paths (dither, tier ≥ 1 winner-take-all) are untouched. The verify-terrain-blend device gate is re-pinned to the sharpened character.

  • Pinned in brush-material-erase-replace.test.ts (mask raised proportionally, erased back down, spec-dropped when emptied, restored by one undo, untouched by additive paint) and terrain-jobs.test.ts (a full-mask texel composes as painted material alone; the mask changes the inputs hash).

  • Perf: flyweight set-event dispatch for motion-class SoA components (speed-of-light row soa-dispatch-flyweight, ruled 2026-08-04). A new SoA-only schema opt-in transientSetEvents routes setSoA3/setSoA4 subscriber dispatch through one pooled event object (+ pooled previous/value vecs) per store instead of minting three fresh objects per dispatching write — the top steady-state GC feeder at 3k-moving-entity scale (~270–500k objects/s → ~0).

  • BREAKING (engine-internal subscriber contract only): for the seven opted-in components — transform/world-feet-position, transform/world-rotation, transform/world-scale, physics/body-position, tome/local-feet-position, tome/local-rotation, tome/local-scale — an onComponentSet event object retained past the callback is overwritten in place by the component's next dispatch. Every in-tree subscriber of these components was audited (spatial-index, hierarchy-solve, hierarchy-render-solve, local-transform-projection, rapier/sync, aoi-index, sprite-raycast, perception, nav-grid, overlap-probe-cache) and reads fields synchronously; onComponentSet is not reachable from creator scripts or any run_script/ObjectAPI surface, so no session-facing behavior changes and no banked-claim falsification exists for this change. Re-entrant same-component writes fall back to fresh mints; dev-mode mutation tracing forces fresh mints throughout. Contract: src/engine/ecs/contract.md (Subscriptions), pins: src/engine/ecs/__tests__/soa-dispatch-flyweight.test.ts. No pre-existing test pinned the old fresh-object identity for these components, so no pins were flipped — the new pins are additions. Savi-latency cluster (squeeze-hunt round 2 — the apply path's second act after the changed-set-first visit filter): a non-short-circuited applySpec re-derived spline/room lowering for the WHOLE world on BOTH diff sides, re-ran ~6 whole-spec derive/library sync walks ungated, rebuilt every per-apply container, and every attributed ObjectAPI/fold spec write copied the ENTIRE object-diff-signature map to evict a handful of keys.

  • Per-row lowering memo (src/tome/lowering-memo.ts): generative-spline (road/powerline/stairs/coaster/preset/fence/hedge/pipe) and room rows reuse their lowering products by reference when the source row (by identity), place mode, installed terrain definition token, and the composed terrain:height chunk-version fold over the entry's padded bounds are all unchanged. The field token is the SAME fold the spline redrape watcher re-lofts on (shared helper), so "the ground moved" can never mean two things. Scripted-kind splines, generator-script-bearing rows, scatter rows, and lookAt rotations are excluded and re-lower fresh every apply, exactly as before. Eviction edges: identity replacement (ObjectAPI/COW-fold writers), the spline drag preview's sanctioned in-place row write (explicit evict beside the diff-signature one), terrain def/field tokens, per-apply pruning to the final new-side expansion, whole-memo drop on a failed apply.

  • Old-side expansion reuse (TomeAppliedGeneration.expansion): under the delta-0 gate's own completed-apply binding (specRef === live doc) plus expansion-time keys (residency, place-instance content, player-body derivation inputs — a new computePlayerBodyDerivationKey covering active players and session-parent liveness — and the unfiltered-record identity), the next apply's old diff side reuses the previous apply's FINAL new-side expansion whole, containers included (flat list, byId, placeById, id set). Omitted whenever a survived-destroy retry row diverged the recorded doc from what was expanded. Old-side lowering failures stop double-reporting (the recording apply's new side already reported them once).

  • Caller-mutation guard: everything recorded past an apply (memo source rows, expansion defs) is re-bound to the ENGINE-owned clonedSpec row twins — "row identity ⇒ content unchanged" holds for immutable-by-replacement engine docs, not for a caller's own doc mutated in place and re-applied (the pinned in-place scatter-count editor pattern stays a full visit).

  • Diff-signature eviction side index (object-diff-signature-invalidation.ts + TomeObjectDiffSignatureIndexResource): a root-id → key bucket index built once per apply beside the signatures map makes attributed spec-write eviction O(touched roots) with in-place deletes, replacing the per-write whole-map copy + full key scan. Unbound index (any non-apply map identity) falls back to the historical O(N) scan-into-fresh-map path.

  • Derive-pass skip gate (TomeDeriveSyncInputsResource): the six library syncs (materials, field-feed demand, authored clips, looks, textures, warm hints), the spec-shape warn rail, and the asset-manifest merge are skipped when every raw subtree they read (scripts/places/assets/camera/player identities) plus the row-derivation keys match the last completed apply — with per-pass derived-component presence checks so projection resets (which recreate the tome/spec entity bare) always re-derive. Runtime ensure-hooks, the renderer join-watchdog force lane, and TomeSpec reset hooks call the sync functions directly and never see the gate.

  • Container floor: buildObjectDiffSignatures returns the previous map by identity on a 100% hit (consumers pair every signature lookup with an oldObjectById lookup, so a superset map is safe); the reused old side ships its containers from the generation record.

Bench (spec-fold-cost.test.ts, new apply-path case: 1,060 source objects — 20 roads + 10 powerlines + 10 fences + 20 rooms + 1,000 flat — through the fold lane): delta-1 apply median 51.5ms → 12.4ms (expand-old 15.3→0.4ms, expand-new 14.8→0.4ms, signatures-new 15.2→2.6ms); 200-write burst 439µs → 112µs per write. Anchor (ecs/soa read-ts-plain) 13,107ns.

Named residues, deliberately not built here: the compiler's second child re-expansion per apply (compileSpec runs before expansion — reordering is its own change); pooling of genuinely apply-local scratch (small, and reentrancy-sensitive); the rebind-candidate query walk. Resident-memory note: the generation record now retains one expansion's containers and the memo retains lowering products between applies — bounded by the currently-expanded world, released on prune/replacement.

  • GLB texture payloads stop pinning whole files (jetsam cluster, round 2). parseGlb minted texture bytes as views over the GLB input buffer, so any retained parse pinned 100% of the file — geometry, JSON, and texture payload — for the retainer's lifetime. Three cuts along one seam: (1) GLB-embedded image bytes are now OWNED exact-size copies, so the fetch buffer collects after parse (renderer-side retained drops from whole-file to texture-payload-only; the budget estimator's texture term becomes honest by construction); (2) the sim asset service parses with retainTextureBytes: false — the sim worker never decodes or uploads textures (reader census: zero sim-side consumers), so the texture share (often 50–80% of a textured GLB) never becomes resident in the worker-lifetime modelsById cache at all; (3) the renderer models registry releases parsed.textures[].bytes once every decode for the asset settles — the GPU copy is the source of truth, device loss is a terminal reload wall (recovery re-fetches + re-parses), and assets are created once per modelId per scene, so no in-session reader remains. Receipt (8MB-texture synthetic GLB, reachable-backing-buffer arithmetic): old = whole file pinned per retained parse; owned-copy = texture payload only, input unpinned; sim flag = ~0; post-settle release = ~0.

  • Render-channel writer scratch sheds its burst high-water. The spill-encode scratch doubles toward MAX_ARENA_BYTES (512MiB) during boot/backlog bursts and was never re-minted smaller — the high-water stayed resident for the session (the transport-residency stack named in ecs-sync's collapse-gate comment; PR #12099 iPhone-16 jetsam kills). It now re-mints at its 256KB rest size after ~30s of active sim ticks with zero spill demand and an empty pending backlog (frames never alias scratch — pushScratchFrame copies). Re-materialization is the existing cold-path shape: the next spill's ensureScratch(16MiB). New getScratchCapacityBytes() observability getter.

  • Preload-hint derivation survives metadata-only spec landings. collectSpecPreloadHints memoized on whole-spec identity, so every asset-metadata landing (bounds batches, the per-analyzed-model triangle stamp, socket/parts rails — dozens per boot, plus every landing mid-session) paid ~5–7 regex passes over the entire script corpus plus place/object walks to re-derive an identical answer. The four metadata writers now land through one shared helper that forwards the memo when every derivation input identity is preserved (input set positively bounded: collectSpecAssetEntries reads places/terrain/atmosphere/objects/player/scripts and never spec.assets; sprite-warm-variants reads scripts/places/objects/player) — a hit returns the IDENTICAL array, so the fast path's previousRefs delta stays a no-op.

  • Sidecar host-derive rails stop walking the world. Both rails' ~1s scans iterated every TomeParent-bearing entity (the whole parented population) on every metadata-authority client — including all singleplayer sessions — even with zero socket/part attachments. A hook-fed per-world attachment index (TomeParent add/set + one-time seed; the BoundsPrefetchFeature needs-set shape) makes the scan O(attached-children), i.e. zero for the overwhelming majority of games; stale ids prune on visit. Riders: the per-tick completed.splice(0) empty-array mint is gated on length; the authority resolution (place-hosts read + session query walk) is skipped entirely on ticks with nothing to scan or dispatch; tombstoned entries now cost two boolean checks per tick instead of nested spec probes (resolved-entry cleanup — including a tombstone's cap seat — rides the scan-cadence sweep, with a dispatch-due re-check so a resolved pair is never fetched).

  • Perf: squeeze round-2 jetsam cluster (half A) — budgets and eviction for decoded media, behavior-preserving (sol-table .tmp/squeeze-hunt-2026-08-04/round2-rows.json). Decoded audio PCM gets the aggregate budget MAX_BUFFERED_CLIP_BYTES never was: the resource service keeps a raw-decoded-bytes ledger in LRU-touch order and, when a decode crosses the per-device budget (AUDIO_DECODED_PCM_BUDGET_BYTES in perf-static-data.ts — 128/64/48 MiB by the texture/model keep-alive facts, the table's third instance), releases oldest-touched buffers first, pinning the WebAudio renderer's live voices + pending starts (and the just-decoded clip, so one over-budget clip can never decode-loop). Only handle.buffer drops — handles, stream elements, duration memory, and failure/park budgets survive, so an evicted clip re-decodes on demand through the exact paths that loaded it; the re-decode latency on a cold retrigger is the traded term. getStats().decodedPcmBytes is the gauge. The video warm cache's flat 64 MiB budget becomes device-tiered through BOTH construction sites (the shared main-thread cache and the tome-UI frame warm lane): VIDEO_WARM_BUDGET_BYTES 64/32/16 MiB — a mobile miss is the module's own documented designed degradation (plain JIT streaming). Renderer-side: runModelEviction gains an exact O(1) early-out (running totalCachedModelBytes maintained at the cache's set/delete/clear seams; unreferenced ⊆ cached, so under-budget frames skip the per-frame walk + sort + array mint — getModelEvictionStats().cachedModelBytes pins drift). fx particle rasters stop being a session cache: they release by poll recency (every consumer — pack compositor, content-scale measurement — polls per frame until it settles, so 5s of silence IS the settled signal; "unavailable" tombstones stay as the refetch-storm guard), and a composited pack drops its captured per-layer CPU rasters instead of pinning them for the pack's lifetime. PipelineCache gains pipeline-key retention: material instances retain the variant keys they mint and release them on destroy, so an edited-away scripted material's driver-compiled PSOs leave the cache with the record (scripted-lane idle-LRU eviction, tilemap/decoration scripted retire, booth cleanup — all one seam) instead of living to device loss; drops bump generation so draw-memo pipeline-handle memos re-request (a map hit for survivors), a booth previewing a live material holds its own retain, and un-retained keys (fixed shaders, post chain, lanes) keep session lifetime exactly.

  • Named residuals: the audio ledger counts raw PCM only — a looping clip's conditioned copy rides the raw buffer's lifetime and roughly doubles its true cost (the budget is deliberately conservative against it). A raster whose pack died mid-fetch stays resident for one grace window before the sweep drops it.

  • Perf: speed-of-light squeeze wave 3, tick spine + timers (sol-table .tmp/squeeze-hunt-2026-08-04/2026-08-04-v2-whole-sim.md). The client runtime finishes the server's sharedCtx shape: per-system SystemContext objects, state closures, and push/pop run closures are cached at rebuildPhaseLists (tick/dt/jobs re-stamped per run), ctx.rng becomes a per-run lazy getter deriving the identical (epoch, tick, phase, name) fnv seed on first read, per-phase reduction stagers are prebuilt like the server's stagerByPhase (killing the per-phase flatMap and its reductions ?? [] empty-array mints), and Diagnostics.runWrapped writes a pooled verdict object — the ~6-7-alloc-per-system-run mint chain drops to ~0 and the 120-no-op-system spine bench (runtime/client-spine, new) goes 146.8µs → ~11µs/tick as measured during the hunt (loaded box); the #12166 adversarial re-derivation measured the identical paired bench at merge-base vs head on a quiet box as 79.2µs → 21.4µs (~3.7x) — the win is real, the headline ratio is measurement-conditioned. LastReductionStager memoizes its Proxy + boundFnCache (stable world identity for reduction-declaring systems across ticks — pinned by reduction.test.ts) and takes a prebuilt Set for the reducible-membership probe. Timer dispatch drains a (dueTick, id) binary-heap index (tome/timer-heap.ts; armTomeTimer is the one arm gate) instead of scanning every armed timer per tick — cancels/reaps stay map-only deletes dropped lazily at pop, holds/spills re-queue under their original key, fire order stays byte-identical ((dueTick, id) pins added to timer-dispatch-budget.test.ts), and the 5k-armed idle-tick scan collapses 6.1µs (reconstructed-old) → ~70ns (~90x; new bench case) — the #12166 re-derivation measured the real old scan at 88.8µs on the merge-base tree, so the paired ratio is ~897x, an order better than the reconstruction suggested. Small kills ride along: the server's per-tick sharedCtx/noop-jobs/placeholder-rng mints hoist to module/runtime scope, sim attribution keys per-kind maps by raw name (no per-sample NUL-template string), the client sim-step timing window becomes parallel Float64 rings (no per-tick sample objects, no Array.shift), and the worker's controlled-entity change probe compares retained expanded lists via scratch arrays instead of minting fingerprint strings per frame.

  • Named residuals: a dead owner's timer now reaps at its dueTick (dispatch's hasEntity gate) instead of eagerly per tick — closure retention until due is bounded, and the spill breadcrumb's armedTotal counts such timers until then (log payload only). Reduction proxies are stable across ticks by design — the identity-per-tick pin in reduction.test.ts flipped to a stability pin. Static-body rotation ordering (ledger 2028, savi filing 61538a0c / Streetwork): under replication an entity's components arrive as individual rows, and chunked bursts + flood-budget re-delivery put tick boundaries anywhere in the row set — so a static body can materialize from its physics/body-config row before its transform/world-rotation row arrives, born wearing identity. The dig's churn repro (adopted here) shows current master converges those orderings, but only through a stack of four mutually-covering accidents (the write-back's cache-unchanged guard, the #550 transform-replace compare, the state re-align before dispose, dispose clearing caches). This change replaces the accident stack with an invariant: a static body's rotation is a pure function of its replicated WorldRotation — physics never authors it, and every (re)materialization derives from it.

  • initializeBodyState: statics read WorldRotation (the author row) before PhysicsBodyState.rotation (the physics echo) — the previous precedence rebuilt statics at whatever stale orientation the last write-back caught unless the caller remembered to re-align state first. Dynamic/kinematic precedence unchanged (physics owns their rotation; state stays first). Engine-managed statics with no WorldRotation (terrain chunks, tilemap colliders) keep their state-seeded behavior.

  • applyStateToComponents: statics no longer write WorldRotation back to ECS. The write-back used to mint WorldRotation=identity onto a mid-adoption static, occupying the author's replicated slot with an echo — upload fabric for ownership write-fights on client-auth hosts, and one component-sourced cache seed away from permanently blinding the transform-replace compare. The rotation cache still records the realized body pose (it is exactly the "realized" side staticTransformReplaceNeeded compares the author row against).

  • getOrCreateHandle (signature-change swap): a static whose colliders are about to be re-created first gets its body pose re-written from the author components (WorldRotation, feet-first translation), so the fresh colliders index at the true pose — the swap used to inherit whatever pose the body was born with, so a signature row (scale, config re-delivery) co-arriving with a late rotation re-indexed fresh colliders at the wrong orientation and re-marked the signature fresh, permanently. The swap itself deliberately stays in place (no dispose+recreate: a removed-and-reborn body is not query-visible until the next step — a one-tick hole for raycasts and CC grow probes — and terrain edits pin handle identity); character capsule-grow holds and the mesh-not-ready hold are untouched.

  • Static birth pose is author-derived end to end: the body desc and initializeBodyState both resolve a static's translation feet-first (WorldFeetPosition is the authored surface; BodyPosition is the realization echo, stale across a component-written move) — previously a static rebuilt after a component feet-move could be born at the stale center whenever the slept-pose coherence check didn't fire, and its colliders spent the rest of the tick indexed at the birth pose.

  • seedPhysicsCachesFromECS: never seeds a static's rotation cache from ECS components (that is the one writer that could make cache == component while the body wears something else, silencing the compare forever). Currently test-only; guarded so it stays safe if resurrected.

  • Suite: dig2028-rotated-static-box.test.ts — the dig's two straight-path probes (drive across the world AABB unblocked / into the oriented face blocked), the staged churn repro (config-first adoption → reap → re-adopt, rapier cuboid halfExtents + body rotation inspected at every stage), and the signature-co-arrival poisoning case. The no-phantom-mint assertions are red on master before this change.

Named residues, deliberately not built here: (a) the synthetic feet-delta contact.normal savi burned probe cycles on — already eliminated by the fabricated-contact fix (ledger 2022, #12153, landed); (b) the owned-entities write-fight (~38k fenced rows in the filing room — netcode/command-dispatch vs the hosting client over tome/owned-entities) — the environment that tears adoptions mid-entity and can fence the rotation row out entirely; rows 2016/2017 ownership family. When the rotation row never lands in ECS, no physics-layer fix can dress the body correctly — this change guarantees the body converges to whatever the author row says the moment it exists, and that physics never fights the author for the slot.

  • Terrain textureScale adopts meters-per-tile (the object-material semantics). The heightmap material pack conversion (features/terrain/config/factory.ts) previously computed shader repeats-per-meter as 0.4 × textureScale — a repeat-frequency multiplier inverted from object materials' "meters each tile covers", ~1000× denser for the same small value; the knob-name collision burned a creator world authored at 0.05–0.08 (thread 1785625835). Now metersPerTile = textureScale (clamped ≥ 0.001), shader repeats/m = 1 / metersPerTile. No stored-value conversion (jacob's ruling, ts 1785626860). Defaults and tile-only materials pack byte-identical values to pre-flip; savi docs (api-reference §TerrainDef, heightmap-terrain skill) carry the flip note + conversion.
  • Held keys no longer survive focus/mode transitions (row 2480, the phantom-movement strand). raw-capture's overlay gate (shouldCaptureKeyEvent) drops keyups the instant the input mode reaches "overlay", so a key pressed mid-stride and released over the chat box never delivered its KEY_UP — the sim's keysDown kept the key and the character self-walked until something else happened to fire a RESET. Document blur could never be that something: the overlay runs on proxied events and the iframe keeps focus. The sweep now fires at the CAPTURE transition itself — raw-capture subscribes to input-mode changes for the life of the capture session and runs the same silent RESET the blur/visibility/text-entry-focus seams use (no synthesized release edges, so no activeOn:"keyup" binding fires from opening the chat; a key held straight through the overlay stays dead until re-pressed). tab-held deliberately does not sweep: keyups still capture there, and a Tab chord mid-stride must not stop the walk (#13173's tab-timing lane).
  • God-mode exit clears held input, mirroring the entered edge. applyWorkerGodModeExited now runs the same clearInputState the entered edge always ran — keys held while flying the god camera no longer walk the player the instant control retargets.
  • The gamepad overlay park collapses into the transition sweep. parkGamepadForOverlay's own RESET + axis-cache drop were the per-source copy of exactly what the sweep now does at the transition; the poll can't reach overlay mode with un-swept held state (state only accumulates while the poll emits, the poll parks before emitting in overlay mode, and every entry into overlay runs the sweep), so the park reduces to the overlay early-return and the gamepadOverlayParked flag is deleted. Pad park semantics are byte-identical: one silent RESET at overlay entry, axis levels re-emit on exit, button edges never refire.
  • A non-takeover engine exit's unlock event is now itself a reconcile edge (dig 62093b4f, row 2479 — kent's 64s dead mouse-look, jacob's stuck moment). The pointer-lock reconcile is edge-triggered and never polls, so an engine-counted exit whose unlock EVENT landed after every relock-bearing edge had already fired — overlay flap → exit issued → mode returns to game (reconcile no-ops against the still-true lock mirror) → unlock delivers — left a lock-preferring game with dead mouse-look until the next overlay cycle. The unlock of a counted exit now reconciles: the exit was ours, the browser permits the silent re-request, and canAcquireLock re-checks desired state so an exit whose reason still holds (open cursor UI, withdrawn intent) stays out. TAKEOVER exits — touch release and parent break, which hand the pointer to another owner against desired state — are flagged in flight (takeoverExitInFlight) and keep today's behavior: their own unlock never relocks, the next reconcile trigger decides. User exits and browser revokes still arm the gesture gate, untouched.
  • The relock decision on every game-mode unlock ships to DD (probe #1 from the dig). The Lock changed edges always shipped; WHY no re-request followed was the blind spot. One [pointer-lock] Unlock decision (mode: game): … debug line names the outcome — re-requesting, gesture gate armed, authored intent withdrawn, cursor UI needs pointer (N surface(s)), takeover exit, not lock-capable, page hidden — so the live-watch query for the wedge signature closes on a reason instead of an absence.
  • mouse4/mouse5 join the input-binding vocabulary (r-c5a11032, kent's tactical shooter; tucker's independent "No thumb buttons?"). inputs.actions.*.mouse (and modifiers) accept the side/thumb buttons: token mouse4 = MouseEvent.button 3 (back), mouse5 = button 4 (forward); aliases mb4/mb5/back/forward. The internal names follow the DOM index (Mouse3/Mouse4 — input-resolution's generic Mouse\d+ lane and raw-capture's BUTTON_MASK already carried buttons 3/4), so the engine half is pure vocabulary + the nav claim below. mouse3 deliberately resolves to nothing: DOM numbering says back, gamer numbering says middle, and an ambiguous token that guesses wrong rebinds someone's weapon. Side buttons also work as held combo modifiers (MOUSE_BUTTON_NUMBERS 3/4).
  • Bound side buttons suppress browser history navigation. A game that binds mouse4/mouse5 claims the button for the session: raw-capture preventDefaults the pointer events AND the compat mousedown/mouseup (Blink keys the navigation off the unprevented mouseup) for exactly the claimed buttons — derived from the installed bindings in installInputClient, the same wiring as the gamepad right-stick claim. Unbound side buttons keep browser back/forward untouched. Known residual: the authored-realm forwarded lane (realm iframe, unlocked pointer) does not yet suppress; pointer-locked play — the tactical-shooter shape — delivers natively and is covered.