engine v5.2.18
Engine v5.2.18
August 5, 2026
A patch in the Lume line.
what's new
- 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.
- Changes your game's scripts make to the game's own setup — input bindings, UI, camera, terrain settings, and more — now survive saves: a script could change these live and see the change silently erased the next time anything else saved the game. Now they stick.
- 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.
- Particle effects can finally layer in front of 2D backgrounds — spells, explosions, and sparkles in a 2D game draw in front of backdrop art instead of always hiding behind it. Effects and sprites now sort together by world position, so an effect between two scenery layers lands between them.
- Heat-haze and shockwave-style distortion effects no longer flash a ghostly rectangle that erases the effects behind them when their ripple settles to zero — an idle distortion is now invisible, exactly like it should be.
- Game databases work in relay multiplayer: scores, inventories, and anything your game saves with SQL now actually persists on relay rooms — including schema setup, which happens automatically even in rooms that are nothing but players' browsers.
- Performance readings got honest on iPhone: a GPU timing quirk could report absurd frame costs (literally the phone's uptime) — those readings are now filtered out at the source, so performance snapshots tell the truth.
- Games sit lighter in phone memory: an idle world no longer keeps ~24 MB of background job machinery resident, which means fewer iOS tab reloads on long sessions.
- One over-sized background job can no longer silently break all terrain and asset loading for the rest of a session — it now fails alone, loudly, naming what got too big. And the "server physics is paused" notice stops promising a wait will fix itself when it provably won't — past the warm-up window it reports the measured pause and routes to a bug report.
- Timed entities actually expire now, even when a player's device wedges: projectiles, pickups, and effects spawned with a lifetime no longer pile up frozen in the world when the machine simulating them stops responding — the server cleans them up at their deadline.
- Changing another object's state now just works everywhere you can see it: edit
.stateon 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.
- Editing scripts on big worlds feels snappier: each save no longer recompiles every unchanged library module from scratch, so the little freeze after an edit on script-heavy games shrinks to just the code that actually changed.
- Pinch zooms the game, not the page: every game gets two-finger pinch (and mouse-wheel) zoom by default — camera scripts that read the zoom channel keep full control of what zooming means, and games whose cameras ignore it get a sensible built-in zoom on phones and tablets.
- Big multiplayer worlds got faster to join and lighter to run: players now download and stream just the area they're in instead of the whole room, so joining a large world is quicker and crowded rooms waste far less bandwidth on movement nobody can see.
- Multiplayer rooms open faster: the world download now starts the moment the connection begins instead of waiting for the handshake — worth up to nearly a second on mobile connections.
- Multiplayer movement smoothed out on relay rooms: other players no longer freeze and teleport when they start moving, stop, or change direction — walking in any pattern now streams as smoothly as walking in a straight line always did.
- Music works in relay multiplayer rooms: jukebox tracks, stems, and fades now play for everyone — including players who join mid-track, who come in at the right point in the song instead of silence.
- Script-created things stop vanishing in relay multiplayer: cars, pickups, and anything a script spawns when a place starts now come back correctly when players travel between places — leaving and returning no longer leaves the world permanently emptier.
- Multiplayer ghosts are gone on relay rooms: a player who walks into your area now reliably appears, and one who leaves reliably disappears — no more invisible neighbors or lingering copies standing where someone used to be.
- Relay multiplayer got dramatically cheaper on the wire — player movement and world state now ride a compact binary lane (3–5× smaller), and catching up after joining compresses — so rooms feel smoother on weaker connections and mobile data.
- Big terrain rebuilds can't knock a room over anymore: when a whole world's ground rebuilds at once, the server paces the work instead of freezing — no more "the room restarted and everything spawned in it is gone" after a heavy terrain edit.
- Games with lots of moving objects spend less time in memory cleanup — motion updates now recycle their bookkeeping instead of generating garbage every tick.
- Things spawned right after a save now come alive immediately — on script-heavy worlds, entities created during the few seconds after an edit no longer stand frozen (visible but running no scripts) until the engine finishes recompiling.
- 2D sprite edges are clean again — characters and props no longer wear a thin halo of sky color around their silhouettes when standing in front of tilemap ground.
- 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).
- Heavy particle effects and layered music run smoother — ribbons, trails, and audio-rich scenes shed a large share of their per-frame CPU cost, with identical visuals and sound.
- 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.
- Switching a world's terrain type live actually works now — going from tile-based ground to heightmap hills (or voxels) no longer silently keeps the old ground stuck in place until the room restarts. And if a terrain setup ever fails to install, the log now says which place and what that means, instead of failing quietly.
›technical notes
-
playSound gains
follow: true(row 2037, dig 55122dec). A positioned one-shot that names it mirrors its SOURCE entity'sWorldFeetPositionwhile 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 —spawnPlaySoundrecordsfollow+ 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 anaudio: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 namedtrue(unpositionedfollowteaches once and lowers — non-spatial plays are already ear-locked; loops lower silently — tracking loops are theaudio: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 fromviewState.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. ExplicitAudioListener.forwardoverrides (the 2D fixed-basis law) still beat every camera-derived basis. Pinned red-first inengine/audio/__tests__/prep.test.ts(renderer-preferred, authored fallback, explicit-forward precedence, DrawCamera fallback path). -
Savi surfaces:
followin 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). -
Behavior-rail told-success-never-persisted — class elimination (ledger 2034, dig 4bd40250; second incarnation of the 9b58cad7 patchRouting hole). A spec-doc write from a tracker-less behavior script (
api.patchInputs, and eighteen siblings) applied to the live spec, returned success, and never entered the durable stream — so every strictly-newer durable version rebased the section wholesale (inputs: next.inputs ?? prev.inputs) and wiped the write: annias patched 28 input actions from a behavior manifest and watched four Savi-churn spec applies erase them in 19 seconds, forever, across 190+ versions. The cure is structural, not another hand-copied tail:recordMutationitself carries ONE shared behavior-rail persist epilogue — armed trackers (run_script / god-mode) still own their batch; client-auth behaviors forward source-stamped (the ledger-1653 seam, now tier-complete); everywhere else a mutation classified persistable byisGlobalSpecMutation(the same classifier every other rail rides, with 9b58cad7's compile-time exhaustiveness gate) enqueues for the room's drain. The seven per-verb tails (patchEngine/patchRouting/places/mint) collapse into it, and the nineteen measured holes close: setScript, deleteScript, patchInputs, patchGodMode (+brushes), patchUi, setCreatorTab, patchTerrain, patchPlayer, patchCamera, replaceCamera, addBehavior, removeBehavior, buyables ×3, jobs ×3, replaceSpecWithStarter. A contract sweep derives its required set from the classifier, so a new spec-doc kind cannot ship without a behavior-rail persistence story (or a named live-only exception — patchAtmosphere's session overlay, which durable rebases deliberately cannot wipe). Falsifies: a bound action patched from behavior context vanishing on the next spec version. -
Query iteration SoA component VALUES are flyweights (ruled 2026-08-04, squeeze hunt ECS core). The engine-internal
QueryResultiterator 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 insidefillRow— 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 usetoArray(), no import aliases of the seven SoA components, no multiline query calls, no generic wrapper passes SoA tokens intoworld.query. Zero retainers found, so no consumer migration was needed. The script-facing boundary stays fresh-mint by construction and is now named in theQueryResultdoc:api.querybuilds its own results (query-utils.tsbuildQueryResultcaptures scalars and lazily mints an escaping Vec3), the exec overlay'squerybuilds its own rows from staged values, andworld.getmints per call — the flyweight never crosses the ECS layer. This change is invisible to behavior scripts andrun_script. -
Pin flip (documented per the ruling's duties): the retain-safety pin was documentary — the
QueryResultinterface 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 fromtoArray(). -
drainEventAddsreturns 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 isreadonly 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
changedComponentIdsscratch in place instead of mintingArray.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/querygainsquery/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 achurn/commit+churn/querypair (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). -
fx particles join the 2D sprite sort band (resx dump 09ad1c9c, #12210). In 2D scene dimensions every sprite draws inside the near-camera sort band (sprite-sort-key.ts) — cutouts write band depth, soft sprites sort by band NDC — while fx particles rasterized at physical play-lane depth and sorted in the alpha bucket by view-depth METERS. Both currencies lose to every band draw, so a background sprite on ANY sortingLayer occluded every particle, and no authoring could put a spell effect in front of a corridor backdrop (fx has no sort vocabulary; creators rebuilt particle effects as sprite entities — Afterschool Starfall's own scripts document the workaround). The fix seats fx in the band: particles rank at layer 0 under the ySort law, half a rank behind sprites so exact-z ties keep the sprite in front. Mechanics: sprites-wgsl VS converts clip z to the band depth of the particle's world z in 2D (new frame uniform
sort2din the_pad0slot) so the depth test interleaves particles with cutout sprites per pixel; 2D batch sortDepth becomes the band value of the anchor's quantized z rank (emissive bias rescaled to band units) so blend order interleaves with soft sprites and 2D text; band constants export once from sprite-sort-key.ts, and frame loop, scene-view capture, and pixel provenance all stamp the dimension. Receipts: newfx-2d-layeringrender-harness fixture (2D corridor: particles beat a layer −12 cutout wall AND a soft panel, stay behind a layer +6 prop) — fails both particle probes on the old engine; fx-fusion/fx-soft-distort/effects/part-dissolve/sprites-ysort/emissive-2d all green; unit pins for computeFxSortBandDepth, the sort2d uniform, and 2D batch sortDepth. -
Distort paint gated by displacement — no distortion ⇒ no paint (the #11475 shockwave law extended to distort sprites, r-156774a3, #12213). A
blend:"distort"fragment re-samples the resolved-OPAQUE viewport share — no fx sprites in it — so a zero-offset fragment repainting that share verbatim erased every transparent drawn behind the quad (the kill-explosion transparent-rectangle cutout; a real authored shape — any haze fading its displacement out through 0). The fragment's alpha is nowauthored alpha × tex mask × smoothstep(0, FX_DISTORT_MIN_PAINT_OFFSET_UV = 0.002, |offset|)— the gate constant is the bespoke shockwave's own strength floor (mix(0.002, 0.05, strength)), so every visibly distorting look keeps full authored alpha while the idle quad paints nothing. Receipts: fx-soft-distort fixture gains the erase-probe pair (an additive glow behind a zero-offset distort quad must match its uncovered mirrored twin), and the effects fixture's rear slash becomes a mechanical target (flat saturated-orange body, whole-frame pixel count, orbit-proof — a repainting shockwave plane collapses the count to ~zero). -
The player SQL lane —
api.sqlworks on relay rooms, bound to what the game published (#11685). Relay rooms had no durable write lane:api.sqlreached a door only a credentialed room host could open, so on the tier that is becoming the only tier the taught persistence recipe silently never ran — hooks fired, saves "succeeded", nothing landed;run_scriptwas dead there for the same reason andmigrate()had no seat to run on at all. The lane opens without giving a browser the database: every call a player seat sends is admitted by DIGEST against a manifest kiln publishes from the spec, so a player can only run SQL the game itself contains — never a statement they invented, never one only the game's own privileged seats run — and@calleris a marker the door fills from the relay-verified identity, so a published call cannot be re-aimed at somebody else's row.migrate()runs inside cf-storage in a Worker Loader isolate with an empty env andglobalOutbound: null, triggered by the door's own manifest pull for a spec version it has not migrated yet — no seat, no room, no boot: a fresh relay room that only ever contains browsers still gets its schema. Hook posture is decided per hook: the relay fires onPlayerConnected/onPlayerDisconnected in the player's own browser with no token-class gate, while onPlaceStart/onPlaceShutdown run only on the credentialed seat. -
GPU timestamp envelope — zero-slot guard at the fold + plausibility gate at the latch (r-e03edd0a, dig fc184c35, #12207). iPhone16/iOS26 sentinel units minted gpuFrameMs ~1.5–2.1e6 ms (milliseconds since device boot) because one resolve carried an unwritten query slot — WebGPU resolves those to 0 — and the envelope's min/max fold anchored at the epoch. Two guards: frame-graph.ts
timestampEnvelopeMs/exclusivePassTimesMs/seamAttributionMsskip any interval whose begin or end resolved 0n (0 is 'query never written', never t=0 — deletes the unwritten-slot class at the fold), and renderer.tspollGpuFrameTimingrefuses any envelope >1000ms, >4× the wall gap between settled reads, or non-positive (the cross-base variant the zero-guard can't see) — gpuFrameMs holds the last plausible sample instead of relaying uptime. The device-farm sentinel relays the engine number verbatim (correct) and needed no change. -
Client job pool memory squeeze (squeeze-hunt round 2). The shared-SAB JSON transport is retired: job submits and results now ride structured-clone postMessage both directions, deleting 2.5 MB of resident ring buffers per worker, a triple byte-copy + triple payload pass per message, and the per-submit compatibility pre-walk — measured per-message CPU is a wash (the SAB lane's event-loop wake was a postMessage poke anyway), so nothing gets slower and every message gets one serialization instead of up to six passes. This also makes the ledger-2031 oversize class structurally unrepresentable: postMessage has no channel byte cap, so the 2 MiB-envelope incident shape cannot recur (the clone-fault rejection lane and the 3-attempt requeue bound survive unchanged for the residual fault classes). Separately, the idle teardown now reaps the LAST warm worker too — an explicit reversal of the one-warm floor: an idle world holds zero ~21 MB engine-bundle job isolates resident (was ~23.5 MB carried for the whole session on every iPhone and 1-worker-budget device), while the eager boot spawn still covers the load burst and the first job after a >30s idle gap pays only a parse-only worker respawn.
-
Job-pool poison elimination + server collider-gate honesty (ledger 2031, incident 547f9352). One oversized job envelope (2,110,187 bytes > the 2 MiB shared-channel max) could silently kill ALL client terrain and asset builds: the transport throw landed in the pool's worker-failure handler, which terminated the worker, requeued the same envelope at the queue FRONT, respawned, and resent — an infinite loop (3,077 worker errors in one session) that starved every build behind it until the collider gate force-opened with zero colliders and nothing rendered. Now an envelope that can't ride the transport fails AS A JOB: a submit-time size gate (serialize once — the measured bytes ARE the wire bytes) rejects over-cap envelopes loudly with byte counts, job type, and a payload breakdown naming which arg ballooned; the structured-clone fallback lane (stack-blowing postMessage) takes the same rejection lane; a requeue bound makes ANY envelope that keeps killing workers fail as a job after 3 attempts instead of looping forever; and the worker-side response over-cap falls back to postMessage instead of crashing the worker. Separately, the "server physics is paused… Nothing is wrong — simulation resumes automatically" run_script note is now duration-aware: past a 2-minute warm-up window it names the measured pause duration, says the wait will not resolve itself, and routes to a bug report.
-
Lifetime silence-backstop — the server reaps expired deadlines past a dark-in-fact simulator (r-c8d52362, the 07-30 projectile pile-up, #12211). A wedged client (starved sim worker behind a healthy socket) is Ready, Streaming, unstamped, and uploading nothing; its envelope had no takeover of any kind — not a seat (no re-election), never suspension-stamped (tab visible, socket attached), and the container tier's server seat fallback skips envelopes. A silent successorless-HELD seat deferred the same way. In both shapes
api.spawn({ lifetime })entities outlived their deadline with nobody to reap them — the frozen projectiles piling up in Mecha Blade, and the class the hot drop's own shotfx-reaper.js broom names verbatim.tome/lifetime-reapnow resolves a reap LANE per expired deadline:simulatorandlast-resortstay byte-identical (hooks on), and the new SILENCE BACKSTOP fires on the server when the entity's simulator-in-classification (envelope owner, or the sitting place host) is dark-in-fact — Ready + Streaming + attached, silent on both upload cadences past the health window (the exact isSilent word host retention reads). The backstop destroy is HOOKLESS (the cross-writer destroy PREVIEW's reasoning): a silent client can wake and land its own replay, and envelope deletes are withheld from the owner, so the owner's own deadline reap fires onDestroy exactly once; a simulator that never wakes fires it zero times. Suspension-shaped connections (hidden tab, detach grace) are excluded by the connection-shape gate — a brief alt-tab never moves authority for this reaper either. Red-first pins in the lifetime e2e suite: WEDGED OWNER (envelope, with wake convergence) and SILENT HELD HOST (remainder) — both leaked forever before the lane. The projectiles skill's flagship example now carries the lifetime backstop its own rules prescribe. -
BREAKING: query results and object summaries hand out the live tracked state proxy (ruled 2026-08-04, supersedes #12092's detached snapshot).
buildQueryResultandgetObjectSummary(tome/api/query-utils.ts) return.statefrom the shared per-(world, bag) proxy cache in the newtome/api/tracked-state.ts— the exact objectgetObject()/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) andtrackClientAuthSnapshotState(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
structuredCloneof a result's.statethrows (proxies are not structured-cloneable). Scripts are unaffected — theirstructuredCloneis the unwrapping bridge (script-value-clone.ts; every tracked proxy mint now registers in its proxy registry). The exec result boundary (exec/engine.tsfinishOutcome) unwraps registered proxies before the reply crosses, soreturn 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.jobargs 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.setre-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 ownfastDeepEqualslaw) 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 engineworld.setcallers 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):
currentEntityIdsis memoized against the overlay's own membership funnel (spawnEntity/despawn — the base world is frozen for the overlay's lifetime), the O(spawned×N)includesscan 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 — ≥ 150kincludescompares + 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.tsrewired from the deletedgetMergedTomeStatedoor togetObjectSummary(...).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.tsreference 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.tsheader; overlay memo comments). -
Module compile artifacts cache across applies, keyed by content and realm flavor (#12098). Every applySpec minted a fresh ModuleContext and recompiled every lib module in the entry graph from scratch — export rewrite, float quantization, ASCII escape, and the factory-memo hash, all O(source bytes), synchronously in the client sim worker on every spec echo (the per-edit freeze on script-heavy worlds; the server measured 2354ms for a 68-script world before its slicer). The behavior/generator caches already survived applies; module codegen did not. The artifact — transformed body + memoized factory — is a pure function of the module's OWN source and realm flavor (require() resolves at execution time through the per-apply context), so it now lives in a cross-apply cache keyed like the failure memo (ref + ui/quantize/audio flavor) with byte-identical source validated per hit. Module INSTANCES stay per-apply — state isolation between applies is unchanged.
-
Pinch zooms the game by default (jacob: "i want pinch to zoom in the game not the site"). The zoomIn axis becomes the engine-defaulted platform zoom channel, exactly like lookX/lookY: when a spec doesn't declare it, the compiler injects the axis and its MouseWheelY binding, which also arms the touch draft's pinch recognizer — so the wheel and the two-finger pinch feed
input.axes.zoomInin every game, declared or not (a declaration of any shape still replaces the default wholesale, and taught camera scripts that read zoomIn keep owning the gesture with their own distance/height semantics — including games where Savi wrote the camera read but forgot the axis declaration, which start working). Games whose cameras never consume the channel get an engine default on touch devices: a presentation-layer zoom the camera system integrates per frame and folds into the published view — perspective rigs zoom optically (fov through the tan half-angle), ortho rigs divide the presented half-height — clamped 0.5×–3×, reset on rig swaps and god edges, standing down whenever anything else claims the gesture (a camera behavior reading zoomIn, any spec axis bound to the wheel, god mode's own god:scroll dolly, or a desktop client, where the wheel keeps its desktop vocabulary). DrawCamera stays authored-pure; the pointer ray follows the presented projection by construction. Containment for the same report (the authored-realm iframe carrying the pinch law so Safari never scales the page) landed separately in #11952. -
Relay AOI: place-scoped delivery +
networking.aoiparity — joins and motion stop being O(room) (#12063). The relay journal gains a place column on heads AND journal rows (idempotent self-heal migration; pre-existing rows read NULL=global) with a producer-supplied retag law (one entity, one place, atomically); joins declare places and drain place-scoped (declare(place) ⇒ drain(place), with aPLACE_DRAIN_ALLbackstop); motion rides an EPHEMERALplcpredicate through fanout (fail-open on every axis; interest is socket-scoped, never journaled). Engine side: dial advert + declare flow, keyed-row place stamping with the retag law, drain-bracket admission, receive-all seat pins, and world-sync under scoped delivery (covered-places statements, bounded ghost sweep, placed answer seat, version asymmetry). Parity v2 adds the §13.1 config→lattice resolver on the shared aoi-cells math, emitter motion sharding (≤16 cells + spill), a two-term cell envelope with full-restate declare edges, and the relayBin 4 codec mirror. Net effect: a join downloads its own places instead of the whole room, and per-tick motion fan-out scales with who can see it instead of who is connected. -
Faster multiplayer boot: the relay join's load-bearing spec snapshot fetch no longer waits for the socket welcome. Every fetch coordinate is already parked at worker mount — before the WebSocket handshake completes — so the engine now kicks the fresh-room
latestspec read right there and the join consumes it when the welcome's target matches (fresh-room latest, or a versioned checkpoint pointer whose dbVersion equals the prefetched read's adopted version — immutable content, so age cannot stale it). Any mismatch discards the prefetch silently and fetches exactly as before: same edge route, same 204/failure semantics, same retry ladder, byte-identical fallback. The result is one full edge round trip + spec body download (100KB-5MB) deleted from the serial wsConnected→specApplied boot path — pure overlap with the WS dial and mount chain, worth 150-800ms at mobile RTTs. -
Relay motion split goes per-COMPONENT, and a journaled name is forgotten without evicting the entity (tucker on staging: "the lag/freezes happen on directional changes", #12197). An avatar's pose and its
draw/mixerweights land in ONE change-log row, and the locomotion feature writes the mixer FROM VELOCITY every tick — hold a straight line and the row is pure pose; start, stop, or turn and a non-motion component appears beside it. The lane split ranrow.components.every(...), so one journaled component disqualified the whole ROW from the motion lane, andforgetJournaledMotionthen ranmotionChain.evict(row.id)— dropping the entity from the delta chain entirely; receivers discard rows for an unbound alias FAIL-CLOSED, so the avatar froze at every peer until the re-bind landed, then snapped. Freeze-then-teleport on every direction change. Two narrowings: (1) the split is per component — a row's motion components take the motion lane even when the same tick wrote something that must journal (rows carrying REMOVALS still decline the split and journal whole: the register has no removal list, and splitting a removal from a same-tick re-add would land the pair out of order across two lanes); (2)MotionDeltaAuthorChain.forget(id, names)replacesevict(id)for journaled UPDATES — the journaled component still re-sends in full (the §11.3 rule, preserved at component granularity), but the entity keeps its alias binding (aliases mint append-only within an epoch);evictremains for real deletes. Considered and rejected: reclassifyingdraw/mixeras motion — DrawMixer carries creator-authored one-shots whose startTick bump IS a replay trigger; that trade swaps a stutter for dropped animations. New pin: relay-motion-delta "THE TURN STUTTER" — a journaled sibling forgets only its own basis, the pose keeps its alias, the receiver discards zero rows. -
The relay tier's music lane —
rail.musicworks on rooms with no server world (zoo jukebox+stems silent on relay, tucker 08-05, #12202). On the kernel tierTomeMusicStatehas a single authoritative writer (hooks forwardrail.music, the server executes,replicate:"always"fans out); a relay room has no such seat, so the lane re-lands the same three guarantees in relay vocabulary: WRITE — the hook-running client is the room's only executor of its creator JS, so the verb applies to the LOCAL world at relay egress (the command lane's "music" consumer, keeping the Layer-3 claim-window hold: a rolled-back speculation never plays audio); FAN-OUT + LATE JOIN —publishRelayMusicHeadIfChangedships the local head as one unstamped update row that the relay journals as a keyed head, so live peers converge LWW in the room's total order and catchup replays exactly the latest head to every joiner (the kernel's "land mid-track" story, relay-shaped); CLOCKS — relay clients free-run their tick clocks, so the wire value carriesrelayAnchor {tick, ms}and ingest rebases every anchor into the receiver's clock (live rows' elapsed ≈ transit, journaled rows' elapsed = dwell — late joiners seek mid-track truthfully, landed fades stay landed, and a stop-fade anchored at a longer-lived writer's tick can no longer hold one peer's music audible forever). An echo guard stamps ingest-applied heads as shared so the publisher never re-ships them. -
Relay generated content survives place travel — onSpawn defers on unknown authority instead of skipping (the P1 vanishing cars, #12190). The release blocker ("enter a place, come back, the cars are gone; reload and they're still gone"), measured against the relay's own durable state: the generated child's create IS journaled and STILL stands as a live keyed head after the round trip — zero delete rows, no tombstone. The loss was re-derived locally on every world: the re-materialization apply destroyed the authored chassis (remove-diff → destroyEntityWithHook, cascading to spawned descendants) and re-spawned it with onSpawn SKIPPED, because the place was unhosted at that instant (
isClientAuthObserverEntity's fail-closed pre-table branch) — and nothing ever re-ran the skipped hook; any joiner that DID fold the still-live create off the journal had it reaped by relay-world-sync's ghost sweep, since the incumbent's liveIds were built from the world that just lost it. The cure at the lifecycle seam: "I don't know who simulates this yet" is a WAIT, not a skip — hosted lifecycle hooks defer until authority resolves, then run exactly once. Measurement pins ride in the two-client harness (journal rows + keyed heads), including the A/B that a HOST-authored destroy does journal a tomb (that lane was healthy — it just never fired here). Named hazard pinned:api.spawnonto an already-live id mints a_2duplicate rather than adopting, so any lane that re-runs a lifecycle hook must establish the hook's spawns are absent first. The tier pin is a guard: place residency is deliberately not mounted on relay clients — placeCleanupSystem is mode:"server", so no place is ever unloaded in a relay room, and mounting residency there without local-only teardown first would turn memory eviction into permanent room-wide data loss (the docstring that primed the misdiagnosis — "the place can re-materialize on re-entry" — is corrected in place: it only ever described the spec's skeleton). -
Relay presence on the wire, cross-place removal, and one ordered egress lane — 8 defects across 2 dumps (#12185). The headline cure: the renderer's visible set was EDGE-triggered (SPAWN admits, DESPAWN evicts, every other op gates on set membership alone), while the include predicate (
ecs-sync computeShouldIncludeEntity) was already place-aware and correct — so a peer that left or entered the viewer's place flipped the verdict with no lifecycle row of its own, and the client computed the right answer every tick and never applied it, in both directions.writeIncludeReconcilenow level-triggers the set: it takes the entities whose verdict inputs changed in this tick's drained op window and repairs any disagreement between verdict and membership — despawn what is in and should not be, snapshot in what should be and is not; cost is strictly O(membership mutations this tick), and the nomination list rides the op walk that already retires memo entries. The render-suppression grace (#735's snapshot grace) is scoped and made a level for the same reason. The remaining defects in the set land presence rows on the wire and collapse egress onto one ordered lane so cross-lane reordering can't resurrect a removed peer. -
Relay wire perf ladder — binary keyed lane, drain compression, quantization (dark), command classification, client parse-once (#12046). B1: the keyed-STATE lane goes binary (
BIN_OP_KEYED_BATCH+ single-part register rows) behind the invite's accepts lattice with a two-step degrade latch and the place-bit reserved for AOI — measured 3.4–5.3× on the keyed tick. B2: catchup/retransmit/joinBuffer drains compress (BIN_OP_BATCH_DEFLATEat relayBin 3; ceiling + compaction-hold + drain-serialization replace the sync-drain accident; the live wire stays plain forever). B3: a compile-closed relay command-lane table kills the zero-consumer journal class — 12 leak names stop journaling. B5: motion quantization Q1 (op-10 register, quantize-at-source) lands default OFF, feel-gated on the walk; rest stamps bit-equal live values by construction — measured 1.3–1.6× over the binary lane per frame, 19.7× vs JSON on the seeded trace. B4: client download-path CPU — a parse-once memo door (fold + apply share one decode, ack facts extracted at emission), host-tick register adoption (golden vector pinned three-way), and the world-sync round-trip kill (pass-through translate + stringify-once assemblers). Review fix riding along:beginRelayInflate's async task is guarded — a throw from a session hook mid-drain no longer strands the ingest-busy latch forever (the silence watchdog provably could not heal it: activity stamps before parsing). -
Server terrain chunk-build submissions are budgeted per tick (#12097). Server chunk builds execute inline in the engine isolate's realm (the inline job executor — no worker threads), so a full-place sweep that submitted its whole pending budget in one tick was tens of seconds of back-to-back build macrotasks on the 2vCPU prod container. The tick RPC blew its 10s deadline behind that batch, the shell reloaded the isolate as wedged, and the reboot from the DB spec erased runtime-spawned entities — a spec-write storm turned into "the room restarted and the zoo is gone". The sweep gate (stale-cancel + trailing-edge coalescing, 5.2.15) already kept saves from stacking sweeps; this adds the missing work-side half: at most
SERVER_MAX_CHUNK_BUILD_SUBMITS_PER_TICK(4) non-safety-lane builds submit per tick, so the tick loop keeps breathing between builds and the wedge-reload-amnesia chain can't fire from terrain load alone. Safety-lane builds (player support) keep their priority lane. -
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
transientSetEventsroutessetSoA3/setSoA4subscriber 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— anonComponentSetevent 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;onComponentSetis 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. -
Spawn live through the deferred-compile window (#12096). On the container tier a spec apply defers the whole-graph behavior compile off the tick path (perf-apex L18): the old graph keeps serving while the new one builds in slices. A row spawning during that window had its compiled entry only in the graph still building, so it appeared fully rendered but inert — no onSpawn, no update — until the swap: seconds on script-heavy worlds. Fix at the spawn path, with the pattern spawnStampedPlayerBodyRows already uses: when the serving graph has no entry for a spawning row's behavior refs, compile just those refs (behavior-cache-assisted, typically 1–2 refs) into the serving graph before spawnObject, so the row boots live like every other spawn.
-
2D sprite silhouettes stop ringing in the sky color (zoo relay walkthrough 2026-08-05, #12199). #11597 routed the depth-writing sprite cutout class (the 2D alpha default) through the plain cutout slot; sprites register BEFORE tilemaps, so every silhouette edge texel (alpha in [cutoff, 1)) blended against the SKY still in the framebuffer, and its depth write locked the stale blend in as a ring the tilemap ground could never repaint — orange on Farm Dusk, blue on Village Green, on ALL sprites. Cure:
MainPassDraws.cutoutBlend— blending cutouts get their own fold slot drawn after EVERY contributor's plain cutout, still inside the depth/velocity-owning opaque half (#11597's routing intent — AO, viewport sharing, velocity all unchanged); far-to-near within the lane keeps sprite-over-sprite edges correct. Receipt: newsprite-2d-cutout-edgespixel fixture (magenta-sky leak detector: 1148 leak pixels on the broken routing, 0 with the fix; authored draw/outline sprites still ring their authored cyan). 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 composedterrain:heightchunk-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 newcomputePlayerBodyDerivationKeycovering 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:
buildObjectDiffSignaturesreturns 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.
-
CPU particle + audio lane squeeze (perf, behavior-preserving — squeeze-hunt round 2). CPU-owned particle effects (ribbons, trails, multi-sink, no-WebGPU fallback) drop their dominant GC feeds: the fx closure compiler evaluates every field expression into per-node scratch tuples instead of minting a fresh array per vec3 node per particle per frame (bit-exact f64 math and identical rng draw order, pinned by a master-generated golden digest); the reap pass mark-then-compacts instead of splice-per-death (O(alive) per frame instead of O(alive × deaths), death-record and draw order unchanged); snapshot group writes resolve their draw group through per-sink pointer caches instead of building a
texture:blend:mask:align:softkey string per particle; and ribbon trail rings mutate their evicted slot in place and recycle whole rings through a capped per-size pool. Bench (5k churny particles, one frame = simulate+snapshot): churn 1.22ms → 0.69ms, 5k-sprite pack 0.59ms → 0.26ms, 2k-ribbon 2.07ms → 1.55ms per frame. Audio lane: the vibe transport tick no longer builds every pattern tree twice on the authority (primeBpm survives only on first-tick/pause/resume edges; the steady-state BPM check reads the tick's own evaluation — one pinned consequence: a mid-flightsetBpmchange now re-anchors on the same tick but its new tempo reaches the trigger-window math one tick (~33ms) later; the audible renderer schedules on its own lookahead either way), and skips the per-tick full-source hash behind a string-identity gate (3-vibe transport tick: 16.1µs → 9.0µs). The audio deriver stops rebuilding and comparing every emitter value every render frame — AudioIntent add/set/remove hooks and TomeSpec edges feed a dirty set (vibes waiting on script replication retry on spec arrival, not per frame). Audio prep collects reverb zones once per frame instead of re-walking every zone per emitter (O(emitters+zones), same verdicts), and its per-frame policy/set scratch is pooled. -
GLB texture payloads stop pinning whole files (jetsam cluster, round 2).
parseGlbminted texturebytesas 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 withretainTextureBytes: 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-lifetimemodelsByIdcache at all; (3) the renderer models registry releasesparsed.textures[].bytesonce 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 —pushScratchFramecopies). Re-materialization is the existing cold-path shape: the next spill'sensureScratch(16MiB). NewgetScratchCapacityBytes()observability getter. -
Preload-hint derivation survives metadata-only spec landings.
collectSpecPreloadHintsmemoized 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:collectSpecAssetEntriesreads places/terrain/atmosphere/objects/player/scripts and neverspec.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-tickcompleted.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 budgetMAX_BUFFERED_CLIP_BYTESnever 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_BYTESin 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). Onlyhandle.bufferdrops — 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().decodedPcmBytesis 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_BYTES64/32/16 MiB — a mobile miss is the module's own documented designed degradation (plain JIT streaming). Renderer-side:runModelEvictiongains an exact O(1) early-out (runningtotalCachedModelBytesmaintained at the cache's set/delete/clear seams; unreferenced ⊆ cached, so under-budget frames skip the per-frame walk + sort + array mint —getModelEvictionStats().cachedModelBytespins 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 bumpgenerationso 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 itsreductions ?? []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-configrow before itstransform/world-rotationrow 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 replicatedWorldRotation— physics never authors it, and every (re)materialization derives from it. -
initializeBodyState: statics readWorldRotation(the author row) beforePhysicsBodyState.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 noWorldRotation(terrain chunks, tilemap colliders) keep their state-seeded behavior. -
applyStateToComponents: statics no longer writeWorldRotationback to ECS. The write-back used to mintWorldRotation=identityonto 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" sidestaticTransformReplaceNeededcompares 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
initializeBodyStateboth resolve a static's translation feet-first (WorldFeetPositionis the authored surface;BodyPositionis 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.
- Live terrain-kind flips compile — geometry inheritance is same-kind only (incident 6ac4bd05, #12209). A live tilemap→heightmap flip could NEVER land: the heightmap config build inherited
existing.definition.verticalRangefrom whatever definition was installed, the tilemap def pins [0, 0], and validateTerrainConfig throws 'verticalRange min must be less than max' — on every apply, forever, until a room recycle clearedexisting. The spec said heightmap while the world kept the stale tilemap ground; clients painted the old terrain over the hole (a wisp flip-looped on it for minutes on Fire Nuke Island — 9 server + 105 client definition_failed rows; a second app hit the same wall the same night). The voxel branch had the silent twin: same cross-kind inheritance, no validator throw — a [0, 0] voxel world with no vertical space installed quietly. The rule, already applied at the heightmap lodRanges site, now covers every geometry field in both branches: verticalRange/origin/lodRanges inherit only from a definition of the SAME kind; signature/revision bookkeeping still reads any installed def. A kind change compiles exactly like a cold boot ([-32, 256] default); same-kind inheritance is unchanged (pinned). Loudness (the incident's third ask): a definition that fails to install now names the place and the consequence in the creator-facing runtime log — 'Terrain for place "main" failed to install (…). The place keeps its previously installed ground — or none — until the terrain config is fixed.' — andtome.terrain.definition_failedcarries placeId. Red-first pin: terrain-kind-flip-live.test.ts (flip installs + objects survive, voxel non-degenerate, same-kind inheritance preserved, failure names the place). - Wire-codec squeeze (round-2 hunt): the client netcode meter and the room wire codec stop doing hidden per-message work. The debug transport used to serialize every outbound JSON message THREE times (once for the wire, once for the byte count, once again per bucket for the breakdown) and mint a payload-sized byte array per measure — byte accounting now rides the wire's own serialization (kernel lane: zero extra passes; relay/hold lanes: one counting measure), and breakdown detail (out-lane section splits, in-lane per-packet decode breakdowns and their 15s retained object graphs) is built only while the F8 multiplayer panel is showing it. DD bandwidth totals are byte-identical, armed or not. Codec inner loops shed their per-write mints: PacketWriter.writeUtf8 writes ASCII (entity ids, keys, component names — ~all wire strings) straight into the packet buffer instead of encoding a fresh Uint8Array per string, and the generic object/object-map writers enumerate keys once instead of minting entries+pair+filtered arrays per node — wire bytes byte-identical, pinned by round-trip and reference-encode tests. The server room dictionary's entity-alias map — the room's only unbounded per-entity residency (grew forever under spawn/despawn churn) — now evicts despawned rows once the replication change-log prune proves no future drain can reference them (two-phase mark + prune watermark; revival cancels; aliases stay monotonic).