Spawn
spawn / swhat we're building

pinned

start herewhat spawn isfaqfrequently asked questionsthe betthe spawn bet

updates

engine v5.2Lume2 weeksengine v5.1ConnectionJune 18, 2026engine v5.0For RealJune 4, 2026engine v4.6AtelierJune 1, 2026engine v4.5Surface TensionMay 22, 2026engine v4.4SolidMay 15, 2026engine v4.3GroovyMay 13, 2026engine v4.2ContinuumMay 9, 2026engine v4.1FoundationsMay 4, 2026engine v0.1GenesisApril 29, 2026

pinned

what spawn isstart herefrequently asked questionsfaqthe spawn betthe bet

updates

Lumeengine v5.22 weeksConnectionengine v5.1June 18, 2026For Realengine v5.0June 4, 2026Atelierengine v4.6June 1, 2026Surface Tensionengine v4.5May 22, 2026Solidengine v4.4May 15, 2026Groovyengine v4.3May 13, 2026Continuumengine v4.2May 9, 2026Foundationsengine v4.1May 4, 2026Genesisengine v0.1April 29, 2026
← All posts
← All posts

engine v5.2.5

Engine v5.2.5

July 26, 2026

A patch in the Lume line.

what's new

  • Physics got a big quality pass in worlds using the new physics engine — cleaner collisions, fast objects stop tunneling, vehicles handle better, and things settle instead of jittering. Busy worlds also run noticeably cheaper.
  • You can now stamp marks onto the world: footprints behind a walking character, skid trails behind a drifting car, splats where things land. Marks stick to the world — never to players walking through them.
  • Characters that floated above the ground (some imported models drew way above their own bodies) now stand on their feet, and clicking them works where they're drawn.
  • Saving an object into one room no longer deletes a same-named object from your other rooms — your building stays built.
  • Stuck rooms fix themselves instead of wedging, and games can opt into resuming exactly where everyone left off after a room recycles.
  • Purchases are now stamped by the server, so scripts can't fake who bought something — and as the owner you can test your own buyables for free again.
  • Scheduled game jobs survive restarts and can call external APIs again — daily events and webhooks keep firing.
  • Nameplates and other HTML labels can pin to moving things without any math, worlds with lots of tile art finish loading for new joiners, and letting go of the mouse mid-move always registers.
  • Press F3 for one unified debug console — everything that used to be scattered across debug panels lives in one place.
  • No visible change. Games with lots of scripts (and Savi's emoji in them) hold roughly 10MB less server memory per world, which means more headroom before a big world hits the per-room memory cap.
  • Scripts can declare how often they need to think — export const updateSchedule = { every: 10 } (or { seconds: 0.5 }) runs update() on that cadence, and near: { tag: "player", radius: 50 } lets far-away objects sleep until someone gets close. The engine skips the skipped ticks entirely — no more hand-rolled tick % N gates — and api.setUpdateSchedule({ every: 1 }) wakes a script to full speed the moment it needs it.
  • Worlds full of decorated, interactable-but-idle objects run measurably cooler: scripts that only react to touches, collisions, or timers no longer pay a per-tick cost at all.
  • If your graphics driver crashes mid-session, you now get a clear "reload the page" prompt instead of a frozen black screen — and Savi is told your screen went dark so she doesn't misread her camera tools as a game bug.
  • When Savi's object previews or scene views fail, the error now says why (for example, a lost graphics device), so she can tell a broken shader from a broken GPU session instantly.
  • No visible change. When a player's graphics device dies, the engine's internal report now names the GPU and its memory pressure at the moment of loss, so device-specific crashes get diagnosed faster.
  • Webhooks and external API calls from custom jobs work again where jobs run on the game server (single-player rooms) — fetch() inside a job script reaches the public internet with sane safety limits, so Discord pings, external leaderboards, and third-party APIs behave the way the docs always said they would. On multiplayer rooms, server-context custom jobs keep running on the host player's device, where fetch is the browser's (CORS applies) — unchanged.
  • Logging a huge value (like baked geometry) from a script no longer stalls the world building megabytes of log text — you get a compact summary like [Float32Array(34992)] instead, and everything ordinary logs exactly as before.
  • Night skies are much cheaper to look at — starfields are now precomputed instead of recalculated for every pixel every frame, so GPU power draw no longer spikes when the camera points at a starry sky. Bright stars still twinkle live, and star brightness no longer secretly depends on your screen resolution.
  • Fixed a progress-wipe loop for solo hosts on a bad connection: if your client got stuck in a way the engine's automatic host re-seat couldn't fix, 5.2.4 kept re-seating you every ~20 seconds — and each re-seat reset the place, wiping your unsaved progress in your own world. The engine now tries the automatic fix exactly once; if the same stuck state comes right back, it stops resetting your world and leaves the place frozen (your local progress intact) until the connection genuinely recovers or another player can host.
  • Fixed lights (and other script-animated values) visibly flickering several times a minute in multiplayer games whose looks are driven by behavior scripts — spec refreshes used to briefly snap script-driven objects back to older values before the scripts re-asserted themselves.
  • 2D characters can now stay perfectly straight while they walk. Engine-driven NPCs turn their body toward where they're going, and top-down sprites used to visibly tilt with that turn — walking villagers looked crooked or sideways. Set upright: true on a sprite (or ask Savi) and the art stays axis-aligned no matter what turns the character; cars, ships, and anything that should visibly turn keep working exactly as before.
  • Games can now opt into UI that holds the same size on every monitor. If your HUD was built on a big screen and looks huge (or tiny) on other displays, ask Savi to normalize it — existing layouts keep working unchanged, they just scale to match every player's screen.
  • Uploaded and generated (CDN) models now always get real collision shapes, even when the collider service can't be reached — the engine builds a collision shape from the model's own visible mesh instead of leaving a permanent invisible 1×1 m placeholder box. This ends the class of bugs where props ghost-rolled forever, players stood on invisible boxes, or objects reacted to hits with the wrong shape whenever a collider download failed.
  • On Mantle-physics voxel worlds, placing a block into your own character (or digging and refilling under yourself) no longer freezes you in place forever — the character now pushes out of solid blocks to the nearest opening, the way it already worked on the default physics engine. If there is genuinely no opening (fully buried), you stay put instead of tunneling through blocks.
  • On Mantle-physics worlds, uploaded/CDN models now collide exactly where they look like they should. Before this, a model's collision shapes floated half the model's height above its visual — balls flew through the bottom of scaled models, and dynamic models tipped and reacted strangely because their mass was centered too high.
  • On Mantle-physics worlds, joints now pivot exactly where you author them. Before this, every joint anchor silently shifted by half the body's height, which made motorized platforms shake objects off and hinges swing around the wrong point.
  • Night-sky fixes: the strange vertical chains of duplicate stars are gone (a hash precision bug), and stars stay crisp points instead of soft blobs when the engine renders below native resolution. Star brightness and layout keep their look; individual star positions reshuffle once.
  • The bubble over objects that are still popping in now says "Loading…" instead of making it look like your world was re-generating its stuff on every visit. Nothing is being re-made — already-created things just take a moment to download, and the label now says so. Only genuinely broken things still say "Couldn't spawn."
  • Your game can now declare player-settings defaults (e.g. reduced flashing on by default) and a perf ceiling in your game's settings — ask Savi to "make my game default to reduced flashing" and it's one edit.
  • Players' accessibility choices always win over game defaults — a player who needs reduced flashing keeps it in every game, including yours.
  • Dev-only conveniences finally work the way you'd expect: a script that checks api.getRoomMode() now gets the right answer everywhere — including the code that runs on each player's own machine, which used to always claim "live" even in your build room. Gate test spawners, debug HUDs, or level skips on it and they work for you AND everyone building with you, then switch themselves off in the posted game — no account checks needed.
  • String lights, bunting, and power cables now hang the way you'd picture them — one graceful draped line through the points you place, bulbs and flags spread along the whole run. No more surprise forest of poles around your plaza! If you do want poles under a line, say so and they'll plant themselves along the run.
  • Characters and machines you build from moving parts (each part moved by your scripts every tick) no longer leave a ghostly motion trail behind them — they now look just as sharp in motion as the built-in animated avatars.
  • Your game's UI now runs in its own sealed sandbox. Everything you could build before still works the same way — buttons, panels, saves, sounds, drawing tools, tilt controls — but a game's UI can no longer reach into or break the Spawn page around it, and a crashing UI can't take the game down with it.
  • When UI code does hit the sandbox wall, you get a clear message in the logs saying exactly what isn't allowed there, instead of a button that silently does nothing.
  • Fixed terrain brushes, path drawing, and object handles sometimes only working for a single click instead of a smooth drag in god mode. Drags now track reliably every time.
  • String lights got a glow-up: small warm bulbs that actually blaze, hanging under lines that sag deeper the longer the crossing — like real festoon lights across a plaza.
  • String lights are properly dense now — a blazing festoon of ~2-3 bulbs per meter instead of a sparse dotted line, and you can tune it with bulbSpacing if you want lanterns instead of fairy lights.
›technical notes
  • Mantle physics quality wave (#8579): collision, CCD, contact manifolds, joints, terrain interaction, and vehicle handling all hardened, with a playground exhibit touring the edge cases. Perf: sleeping objects ~97% cheaper, characters ~12% cheaper, the busiest simulation frontier ~52% cheaper; a few awake microbenches pay 10–33% more at p50 but everything stays under 3.9ms absolute. The engine default did NOT flip — places opt in via physicsEngine: "mantle" in their spec.
  • api.decal — stamped marks on surfaces (#9938, #9963): games can stamp footprints, skid marks, and splats onto anything in the world. Round 2 fixes from the first walk: a signed receiver hemisphere keeps stamps off characters passing through them, and a 2:1 incidence bound rejects grazing-angle stamps cleanly instead of smearing them into streaks.
  • Reprocessed skinned models grounded (#9484): bind correction now cancels the full wrapper residual, so re-baked character models that drew meters above their own physics (one spider hovered ~30m up) stand where their body is — bounds, grounding, and picking all agree with the drawn mesh.
  • Build/edit safety: place-scoped spawn folds (#9956) — saving an object into one room no longer silently deletes a same-named object from every other room; explicit material keys replace stale override echoes (#9793).
  • Room & version integrity: the spec JSON always travels with its DB version (#9976) — restarts can no longer forge "version 1" and wedge rooms; rejected-save storm damping at the host layer (#9955 — applies to all rooms at kernel deploy, regardless of each game's engine pin); stale-epoch resync fires under sparse drains (#9824); reset delivery with a chain age bound (#9821); opt-in exact-state resume when a room recycles (#9901); multiplayer visibleCount heartbeat (#9743); cf-edge is the single room-tier resolver (#9492), killing the split-room class.
  • DO-tier room hardening (#9974): spec-miss failures are loud instead of silent, and an unpublished room latches terminal (room.unpublished) instead of retrying forever.
  • Jobs & economy: spec.jobs authoring door (#9887); cron and lifecycle jobs survive the dispatch gate and room restarts (#9867); rail.purchase actor is relay-asserted — purchases are stamped server-side so scripts can't fake the buyer (#9899); owners and editors can test their own buyables free again (#9855).
  • Feel & rendering: resumable texture-bake slices (#9891) — rich tile-art worlds finish loading textures instead of parking fresh joiners; camera lens matrices exposed for look scripts (#9945); world-anchored HTML UI (#8870) — nameplates and other HTML pin to entities without hand-rolled projection math; releasing the left mouse button mid-movement registers cleanly (#9823).
  • Terrain & tile art: ctx.wang scripted tilesets — the drawtile port with parity fixtures (#9834); face-depth voice knob (#9908); validation + median-pin fast-follow batch (#9842); rail.fieldDab — runtime heightmap edits reach the server (#9985).
  • Script cadence: god-mode recompose reaps stale overrides (#9790).
  • Unified debug console (#9917, #9929, #9939, #9957): F3 opens one mega-console — GPU-passes panel, frame-graph inspector wings, windowing — one keybinding story instead of scattered debug surfaces.
  • Server memory/infra: relay-tier Durable Object (#9786).
  • Errors & ops: a missing UI export is a first-class script outcome instead of a mystery (#9879); game-alert flood damping (#9880); server logs are pure JSON so Datadog parses them again (#9816); exec disposition noise closed — the last unclassified emitters are classified with a detector in CI (#9877, #9972), which ends 5.2.4's three-errors-per-script-run log spam.
  • Telemetry & test rigs (no behavior change): voxel edit-latency path tag + teardown counter (#9973, #9975); cross-client voxel convergence oracle (#9866); netcode-canary battery (#9856). Voxel digging feel is intentionally UNCHANGED this version — only measurement shipped.
  • Behavior-source retention hygiene (plan p-300376ee lever 1; V8 anatomy PR #9777). Four moves against the Garden-class compile lane's 15.8MB of retained source text:
    1. compileFunctionFactory memo keys are now verbatim NUL-joined params + <bodyLength>:<murmur3_128(body)> instead of the full joined codegen input — the ~6.3MB Garden key family (misattributed to "V8 compilation cache" by the first anatomy pass; erratum on #9777) drops to ~90 bytes/entry. Keys stay a pure function of codegen input, so startup-warmed workerd realms keep serving request-time compiles as pure hits (DO-kernel invariant 3; verified on a no-eval-flag workerd: startup-compile then request-time applySpec of the same spec, zero EvalError). Eviction semantics untouched.
    2. The transform pipeline ASCII-escapes transformed source (\uXXXX, surrogate pairs kept as pairs) in string/template/comment/regex content, then materializes the result one-byte (engines OR representation widths through slice/concat without re-scanning). Tagged templates, non-Latin1 code positions, U+2028/29, active-backslash prefixes, and emoji-dense bodies skip verbatim. Spec originals stay byte-faithful; column offsets on emoji lines drift, line numbers never.
    3. The 3,021-char _f32 + Math-shim preamble inlined per compile unit becomes a two-line bridge to globalThis.__tomeNumRt (tome/numeric-runtime.ts — the shim now exists once per realm as real TS, semantics mirrored verbatim and pinned by deterministic-math.test.ts). Same two-line preamble height, so authored stack line numbers are unchanged; ~15% fewer chars parsed per unit compile.
    4. scriptSourceHashCache + extractDependenciesCache (content-keyed) clear on invalidateBehaviorCache/clearBehaviorCache, bounding Savi-edit-session creep. The factoryMemo is deliberately never swept (invariant 3).
  • Measured on the do-sim rig (local workerd, 600 ticks): Garden compile lane 15.8 → 4.8MB, fixed cost before entities 40.9 → 29.9MB, afterTicks live set 88.6 → 76.1MB; memo-key string family 6.7MB → gone; fn-source family 6.86 → 3.01MB; sky-king fixed cost 23.9 → 22.1MB; smoke unchanged. Tick-time histogram unchanged (mean 18.2ms both sides).
  • updateSchedule — declared behavior cadence, gated BEFORE the sandbox crossing (plan p-953ea933, PR 1 of 2): the compiler extracts one data export, export const updateSchedule = { every: N | { seconds }, near: { tag, radius } | [...] }, and tome/behavior-update evaluates it before the crossing harness (api materialization, per-entity RNG derivation, juice/intent wraps, watchdog timing) — a declined tick costs a map get and an integer compare. every phases deterministically per entity (djb2 of the entity id — no mutable counters, identical on every simulating side); near snapshots tagged positions at most once per (tick, tag) and compares squared distances (same-place, physics poses quantized like query reads; positionless/camera-attached bearers never wake anyone). dt = cadence-step (every × tick dt; flag: plan open decision 1). Runtime override: api.setUpdateSchedule(partial | null) — per-key merge over the static export, scoped to the calling script, side-local (never persisted/replicated), cleared on despawn. Multi-script entities: pre-harness gate is the most-permissive union; each update-bearing entry re-gates inside the merged wrapper with its own cadence dt. Malformed schedules salvage (never fault the script) with one compile-lane report. TomeBehaviorUpdateDebugResource gains scheduleSkippedEvery/scheduleSkippedNear.
  • Composed update is presence-gated like onNoise: composeBehaviors no longer synthesizes an unconditional update wrapper, so behavior-update's if (!update) early-out actually fires for hook-only scripts (DEADZONE receipt, ledger 1463: 912 of 1,132 scripted objects — 80.6% — export no update hook and paid the full harness per tick for a no-op).
  • Dead-GPU-instance clients now reach the reload wall instead of zombieing (staging dig 2026-07-19). When a browser's GPU process dies (chronic on software adapters: SwiftShader under headless Chrome), device.lost never fires and every createRenderPipelineAsync rejects with "A valid external Instance reference no longer exists" — previously that message matched no recovery rail, so the client sat with a black canvas forever: pipelines parked one by one, preview_object/view_live_scene failed generically on every call, and neither the player nor Savi was told the device was gone. The instance-dropped class now joins GPU_DEVICE_DEAD_RE, and handlePipelineBuildError feeds dead-class build rejections into the dead-device burst rail — a sustained burst posts the #6710 reload wall plus the renderer-device-lost diagnostic (Savi's getLogs names the device failure and warns her eye may lie).
  • Capture failures name their cause: preview_object ("Preview render pipelines failed to build (…)") and view_live_scene (encode/downsample build errors) now append the underlying pipeline-build rejection reason, bounded to 300 chars and deduped across pipelines (pipelineBuildFailureText). The cause-free form cost a 14-call isolation session to attribute to GPU death.
  • Every device-loss report now carries adapter identity + a GPU-memory snapshot (dig 11400612): LumeAdapterIdentity (description + degraded/integrated verdicts) was previously reported only on degraded/integrated adapter verdicts, and the #1101 gpu-memory ledger was never attached to loss diagnostics — so a 14-day latch-row sweep could decompose losses into 3 signatures but never say WHICH machine class died at WHAT allocation pressure. Both halves already live in-process at the loss site; this is attachment, not new collection. The renderer-device-lost diagnostic's data gains adapter/adapterDegraded/adapterIntegratedHint plus exact gpuBuffers/gpuBufferBytes/gpuTextures/gpuTextureBytes/gpuTexturePeakBytes, snapshotted at post time from every death path (device.lost, dead-device burst, storm ladder, boot watchdog). The [lume] GPU device lost console line appends the same context in one bounded human-readable clause (describeDeviceLossContext). The #6710/#6741 once-per-session latch and reload wall are untouched.
  • Restored fetch() inside custom job scripts on the DO tier (ledger 1542; regressed in #7667 when rooms moved containers → DO): scripted-job isolates load with globalOutbound: null, so the documented webhook pattern (fetch(args.url) in a spec.jobs handler) was structurally dead on the room server. Job scripts now compile with a fetch shim that forwards a bounded request descriptor over the existing JOB_IO forward lane, and the room host executes the egress — mediated, never a blanket unlock (tome/jobs/job-fetch.ts owns the policy; RoomJobIo.fetchExternal is the host side).
  • Host-side policy on every forwarded creator fetch: absolute http/https URLs only; internal-shaped targets refused (localhost/*.local/*.internal, private/link-local/reserved IP literals in v4 and v6 forms, Spawn service hosts, and the room's own kiln origin); redirects followed manually with every hop re-passing the policy; the outbound request carries exactly the headers the job set — platform credentials never attach. Budgets ride the existing job-limits vocabulary: 32 fetches per run, bodies capped at the job wire's 16MB payload ceiling both directions (streamed, cancel past cap), ~10s wallclock per request, with the job's own deadlineMs still racing the whole run. Refusals and blown budgets settle the job through the normal error channel.
  • Tier semantics unchanged elsewhere: builtin jobs (sparks, leaderboard) keep their platform-trusted shell-side lane; Bun-container and device-local (browser) job scripts keep ambient fetch. Multiplayer rooms are also unchanged: the relay-tier server never executes creator JS and custom jobs are not forwardable (job-forward.ts job_not_forwardable), so server-context custom jobs there run on the resident host client's browser — the mediated lane applies only where scripted jobs run on the room server (the DO-isolate tier).
  • ecs.inspect component allowlist + typed-array summary tokens (p-300376ee lever 5, C1): the debug snapshot accepts an additive components?: string[] filter; the headless-room-host 2s poll passes exactly the five component names its buildWorldView reads (WORLD_VIEW_COMPONENT_NAMES), dropping a Garden-class poll from ≥35MB of JSON text (geometry inflating 6.4× through index-keyed stringify, plus the 9.25MB tome/spec value) to KBs. serializeDebugValue summarizes ArrayBufferViews at any depth, so bulk replicate:"never" typed-array holders (geometry/bespoke class) stay cheap in the unfiltered walk too.
  • Object diff signatures are compact content hashes (C2e): TomeObjectDiffSignaturesResource stored the full canonical-JSON string of every object's properties/tags/state — ~5.6MB resident geometry-as-text per realm on baked-heavy worlds. Now murmur3_128(canonical) + ":" + length (~39 chars/field); equality-compare semantics unchanged.
  • Allocation-free spec-value equality (C3a): the duplicated specValuesEqual copies (object-api + interpreter) collapse into src/tome/spec-value-equality.ts; a structural walk verdict-identical to canonical-text comparison replaces the ~2×1MB transient strings a baked-primitive rewrite built per no-op check (9.6ms → 0.1–2.9ms, zero garbage). Exotic values (toJSON, holes, bigint) still take the text path.
  • Runtime-log value budgets (C3b): formatLogArg caps per-arg JSON at 8,192 chars with O(budget) pre-cap work; oversize values render honest summaries, big typed arrays tokenize in place so their small siblings still log.
  • Night-sky GPU cost (jure's thread: looking at the night sky jumped GPU power ~10W→40W): NO per-pixel star hash walk survives in the sky radiance shader. Both sky_starLayer 3×3×3 hash walks (~54 gated cell visits per sky pixel per frame) are gone from the per-pixel path:
    • Dense faint layer → the night equirect bake (sky-night.ts, same dirty gating — its params were already in nightKey). Each star deposits its analytic energy through a normalized truncated-gaussian kernel from the same hash field (sky_starSite, shared WGSL). Energy target per Sol's P1-1: native-σ energy × FAINT_DUST_CALIBRATION (defaults to the old path's 1080p/60°FOV rendering — the old analytic energy was resolution-dependent, ~1.5× at 1080p and 2–5× at wider FOV/lower res vs ≥1440p parity; the bake is resolution-independent with one taste knob). Kernel σ floors at 0.7 bake texel per tier (Sol's P1-2: sub-texel σ made per-star deposit a ±30% grid-phase lottery at the 512 tier; at ≥0.7·texel the lattice sum equals the continuous normalization to ≤0.1%, pinned by a lattice test at 512). Stars-off skies skip the gather via a uniform gate (fable F2).
    • Sparse bright twinkling layer → baked star-field lookups (sky-stars.ts, new): the star SITES (not an image) bake on CPU into an rgba32float record texture (direction/brightness/tint/twinkle-phase per star, capacity 2048, ~1400 at max density) plus an rgba16uint equirect index (256×128, K=4 slots per texel, conservative texel-diagonal footprint rasterization — no coverage holes at texel boundaries; overflow drops the dimmest star, measured drop mass <0.1% at default and <1% at max density). Per pixel: 1 index fetch + up to 4 record pairs + the walk's exact gaussian/twinkle math — footprint σ still tracks the screen pixel and twinkle animates live from the baked phase, so nothing about the look is frozen. Rotation-invariant like the night equirect. Rebake rides the same nightChanged dirty mark + an exact-density guard.
    • Costs stated honestly: the per-pixel night path is now 1 index fetch + ≤4 records + grain (the nightVisibility > 0 uniform gate still zeroes the whole night stack by day, per pixel). The BAKE passes themselves run once per night-authoring change as before, and the night bake now carries the faint gather (125–343 sites/texel; one-time, milliseconds-scale, stars-off skies skip it). f16 range: baked dust peaks sit multiple f16 ulp above quantization at the night floor at every tier (2–20 ulp dimmest→median, scaling with authored starsIntensity).
    • Evidence: CPU reference mirrors both forms (sky-star-bake.test.ts, sky-bright-star-field.test.ts, star-field-reference.ts) — per-star flux parity vs the calibrated target at every tier, lattice-deposit honesty at 512, record contents vs the analytic derivation, index coverage, K-overflow policy, fetch-vs-walk parity at real pixel angles (5e-3 f32-packing tolerance). New scripts/verify-sky-wgsl compiles every generated sky shader on a real WebGPU device (GPU machine required).
  • Place-host wedge self-heal is convergence-bounded (ledger 1532, dump 85d9a1cc): healWedgedHostCycle's sole-candidate leg now spends at most HOST_WEDGE_HEAL_SELF_REASSIGN_LIMIT (= 1) same-client pipeline re-runs per wedge episode. The same client showing the same wedge signature (off Streaming, resetDeliveryFailStreak ≥ threshold, uplink live) at the next confirm window after a full re-run proves the re-run does not converge that wedge — the second identical election is refused and the breaker opens: the sitting host is retained (frozen-but-not-wiping, the pre-#9618 5.2.2 shape) instead of being re-seated at epoch+1 every window. Breaker-open retention is leg-scoped: the retention ceiling's unhosted endgame still bounds the hasStreamed=true legs, while the never-streamed join-window leg holds retained-frozen with no ceiling — deliberate and harm-free (nothing adopted there to wipe, the diagnostic already fired, and a second joiner / socket replacement / departure all exit it; pinned by the join-window-past-ceiling test). The 5.2.4 field failure was exactly that carousel: a solo place host wedged on DO-tier epoch adoption (stamps pinned to a dead epoch, so no confirm could ever retire the fail streak) had his place world-reset — and his host writes wiped at the stale-epoch gate — every window, forever. The DO-tier epoch-adoption root is a separate dig; this bounds the amplifier.
  • Loud once, per episode: breaker-open emits one logger.error("netcode.place_host.wedge_heal_circuit_open", { placeId, clientId, epoch, failStreak, … }) (container stdout → DD) plus one Savi-visible runtime-log error entry, and increments the new wedgeHealBreakerOpens tenure stat. Not re-emitted on subsequently refused ticks.
  • Breaker reset only on genuine evidence-retire edges: the per-place convergence record (PlaceHostTenureState.wedgeHealByPlace — server room state, never replicated, no client-visible tick arithmetic) prunes when the wedged client's fail streak retires (live-epoch confirm or socket replacement — the exact edges #9618 defined) or the connection departs, re-arming one legitimate re-run for a fresh episode; a momentary Streaming flicker without a confirm deliberately does not reset it (on the DO tier, Streaming can flip on a void-accepted send the platform dropped — projection state is not delivery evidence). A viable different Streaming candidate never consults the breaker: real election always moves the place and leaves the record latched to the wedged client's un-retired evidence — the record's lifecycle is evidence-keyed, never seat-keyed.
  • Named residual blindness (ledger 1532 root dig): the wedge signature's evidence (resetDeliveryFailStreak) increments only on the transport throw path. A silently dropped reset on the DO tier (workerd ws.send() returns void = accepted; a lost chunk of an over-cap snapshot drops client-side with no nack) builds no streak, so neither the heal nor this breaker engages on that variant — and no carousel spins there either (no signature ⇒ no heal ⇒ no epoch churn); that freeze belongs to the delivery-truth root (Streaming-on-ingest / post-reset ack), dug separately. When that delivered-signal lands it upgrades the signature's evidence source in place; heal and breaker inherit it unchanged.
  • Spec traffic never stomps what the local client actively simulates and writes (the 5.2.4 farm-dusk DURING-PLAY lighting flicker — round 2 of tiger's zoo verdict, video receipt 2026-07-19; round 1 was #9754's resync→projection-reset coupling): in client-auth multiplayer the simulating client's behavior writes move its live world AND its local doc first — the forwarded batch → server fold → spec push is the same write echoed back a round trip later, or staler (resync answers after a delta rejection, uncovered-writer full pushes). spec-sync's apply diffed that pushed content against the local doc, read "changed", and re-stamped live values backwards: on farm-dusk the dusk-sweep sun's state.warm/night and light snapped to a stale cycle phase, every threshold-reading lamp/window/firefly/rim script answered with a wink-off at its own %4/%5 cadence, and the rig visibly flickered several times a minute (the video's exact pop→crash→staircase luma signature; tome.apply.episode lane tome/spec-sync visiting ~80/111 objects in the field console). The fix extends the execution-authority rule (behavior-update's isClientAuthLocallySimulatedEntity) to the spec-apply lane: forwardSpecMutationsToRoomSink records the ids this client forwards as a simulation authority (local-sim-write-recency.ts, id + tick, every namespace shape), and a non-replace multiplayer apply substitutes the LOCAL doc's defs for rows whose subtree was locally written within the 90-tick horizon — the reconciler sees no change there and live values survive. The tracked TomeSpec head keeps the server's exact bytes (delta folds stay anchored); replace applies (revert, #296) keep their full "reset live state to this spec" contract; foreign edits to objects the local sim is not writing land exactly as before. Masking is ROW-scoped: a foreign edit to a hot row is deferred whole — for the exact paths a script keeps writing it was already futile (the script's next write wins), but the row's other paths wait for the row to cool (path-scoped masking is the named refinement, netcode owner's call). Convergence is guaranteed: a masked apply arms maskedApplyTick, and spec-sync schedules ONE unmasked re-apply of the current revision once that apply's hot window has fully expired (the same same-revision re-apply lane the place-instance key drift exercises) — so the local doc converges to server bytes at quiescence and deferred foreign edits land, even when the masked push was the last one before the room went quiet (#9826 adversarial review's convergence hole).
  • Tests: spec-sync-local-sim-authority.test.ts — red on parent (a stale-echo push re-stamped a locally-written sun light 0.45→1.1); pins live property + script state + child-pool survival through a stale push, foreign edits still landing, horizon expiry re-asserting the doc, and the quiescence leg: a foreign edit inside the FINAL hot window converges to server bytes after expiry with zero further pushes.
  • sprite.upright: true — the 2D spin lock (QA field specimen app 3d656cc0 / dig 604e064c): in 2D places the quad keeps its base pose (upright in 2d-side, flat in 2d-top) and never inherits the entity's rotation as in-plane spin, whatever writes the rotation — NPC steering's yaw-to-travel-heading (npc.turn clamps to min 10°/s, so it can never be zeroed), physics, scripts. Before this, every walking properties.npc character sprite in 2d-top rendered tilted toward its velocity with no lever to stop it; Savi's only recourse was a per-tick counter-write that fights locomotion.
  • One property, one choke point: instanceRotationFor (lume sprite pack, keyed + standalone paths alike) composes the base pose from zero rotation when upright is set — exact math, so upright sprites keep the fixed pins bit-for-bit. Property absent = byte-identical existing behavior (top-down cars/ships keep turning). Sprite raycast picking mirrors the lock (fillPose), so the ray hits the un-spun quad the player sees. Render-only: rotation still drives physics/colliders and children. No effect in 3D places — billboard owns rotation inheritance there.
  • Spec plumbing: ObjectProperties.sprite.upright (types.ts) + DrawSpriteValue.upright (carries through sparse merges like billboard) + SpriteSpecSchema.upright (the STRICT sprite write gate accepts it). Skills: 2d-mode teaches the lock next to the yaw-spins-sprites rule; npc teaches that steering yaws the body and 2D character sprites want upright: true.
  • Tests: upright pins bit-for-bit in both 2D modes on both render paths while the property-less twin keeps the spin contract, live toggle restores spin, 3D no-op (billboard default and "none" unchanged), raycast pick parity in 2d-top, sparse-merge carry, strict-schema round-trip.
  • Opt-in HUD viewport normalization — engine.ui.referenceHeight (papercut pc-4958f5f3, jacob's opt-in ruling 2026-07-18): the game-HUD lane applied zero viewport normalization, so authored px land as-is and a HUD designed on a 4K monitor covers twice the screen fraction on 1080p. When the knob is set (Savi sets it — never a default), the game-target tome UI containers (tome-ui-container + tome-game-mount) get CSS zoom = viewportHeight / referenceHeight (recomputed live on resize), and the worker's window.innerWidth/window.innerHeight mirror reports the same reference space (innerHeight reads exactly referenceHeight), so authored px mean "px at the reference height" on every monitor. One resolver (resolveUiReferenceHeight, ui-globals.ts) feeds both sides: off for unusable values, clamp 240..4320. Game target only — god-mode chrome never scales. Knob unset is pinned byte-identical: no style write, raw mirror dims, unchanged render-message signatures.
  • Rides the ui render message (pre-resolved, like font), participates in the dedup signature so a knob-only patch re-posts, applies live via api.patchEngine({ ui: { referenceHeight } }) with no reload. patchEngine's silent-ignore warn lane and the tome-schemas EngineConfigSchema both learn the key (schema lenient on purpose — engine clamps at read).
  • Tests: unset byte-identical pins (dom-host style surface, mirror dims, signature), zoom math at three viewport sizes + live resize + clear-on-unset, mirror reference-space math at three sizes, resolver clamp band, schema round-trip parity.
  • The collider cook can no longer park a CDN model as a placeholder box forever (zoo-mantle local-dev field bug: all five CDN-model exhibits had empty collider meshes — the ?transform=collider variant resolved against the absolute https://spawnfile.io base, every URL answered a dead 404, the §(g) classifier latched {retryAt: Infinity}, and mantle kept the props demoted kinematic followers for the whole session, hence the barrel ghost-rolling 170 m with no contacts and no sleep). Two legs:
    • Same rail as the renderer. The lightweight collider lane already fetched root-relative /cdn/ through the assetFetch rail; the asset-service lane (service.ts loadModelAsset, which getModelCollider rides) now fetches through assetFetch too instead of ambient fetch — identical in browsers and on the Bun container tier (no route installed ⇒ assetFetch IS fetch, and the container keeps its legitimate SPAWN_API_URL-based absolute resolution for ambient egress), while under the engine-isolate posture (globalOutbound: null) model loads now ride the shell's fetchAsset rail (public-default origins rebase onto the room's kiln, shell-minted identity) instead of failing unconditionally.
    • Local cook from the visual bytes. When both Magic CDN collider lanes settle with a §(g)-terminal-shaped verdict (dead 4xx / empty cook — never the self-healing parked classes, never the rail refusal), fetchMagicCdnCollider now cooks hulls from the VISUAL GLB the renderer already fetches: the lightweight extractor on the variant URL minus the transform param, then the asset service's own model path. To make that possible, gltf-hull-extractor.ts decodes EXT_meshopt_compression bufferViews through the vendored wasm decoder (lume/model/meshopt.ts initMeshoptDecoder, injectable per the MeshoptDecoderLike pattern; refusal preserved only for wasm-less runtimes) — every Magic CDN visual has been meshopt-compressed + KHR_mesh_quantization (normalized int16 positions, dequantization riding nested node scales) since the v7 corpus republish. Result: one quickhull cloud per primitive — coarser than the CDN's multi-hull decomposition (which stays preferred whenever reachable) but a real shape. Terminal-park classification is gated on the same predicate (readTerminalColliderVerdict), so a permanent park now truthfully means BOTH the variant and the local cook are impossible; a collider-cooked-locally runtime log names the degradation direction once per cook.
  • Regression coverage: meshopt+quantized extraction with nested-scale baking red→green pinned in __tests__/gltf-hull-extractor.test.ts (real meshoptimizer encoder → vendored decoder, plus the wasm-less refusal), and the cook rail in systems/__tests__/collider-assets.test.ts — dead-variant/fetchable-visual recovery on the renderer's root-relative URL family (server and client lanes), empty-variant recovery (narrowing ledger 1491 to "empty everywhere"), and the terminal family updated to prove the park only latches when the visual lane is dead/empty too. Verified against the real barrel visual GLB by manual probe (17 729 verts → 0.687 × 0.900 × 0.687 envelope, matching the served collider fixture).
  • Out of scope, noted for follow-up: a ghost-roll stop condition for demoted kinematic followers, and a distinct debug-draw style for locally-cooked (degraded) colliders.
  • Mantle character controller no longer wedges permanently when a live voxel edit closes solid around the capsule (staging field report: "permanently stuck in one place, no movement works" in the mantle voxels place after break/place edits). Two root causes, both fixed:
    • The collide-and-slide verify sweep accepted initial-overlap (toi 0) hits as blockers. Every surface already overlapping at the start of a move iteration is inside the contact-collect radius and therefore already constrained by the velocity-plane solver — including the depenetration escape — but the raw overlap normal of a second overlapped voxel box could oppose the solver's escape direction, scaling the whole displacement to zero every tick, forever. The sweep now excludes initial overlaps (move-shape.ts VERIFY_SWEEP_OPTIONS, the cast API's existing includeInitialOverlap: false); surfaces newly reached along the path still report toi > 0 and block exactly as before.
    • Per-box EPA picks each greedy voxel box's own minimal exit blind to its neighbors; when a real wall opposes that exit the plane system is infeasible and the solver legitimately yields zero displacement. New union-aware voxel unstick in controller.ts (voxelUnstickInto): when a contact is deeper than padding + half the capsule radius against voxel geometry and the capsule overlaps solid cells, the solid bitset is searched for the nearest capsule-sized air pose (current column + 8-neighbor ring, at the current feet height and successive cell tops, bounded 2.5 m climb) and the pose moves toward it at the depenetration speed cap. No air pocket in reach ⇒ no move — a buried capsule never ratchets upward through a solid column (rapier controller escape parity, its pinned behavior). Deterministic and stateless: a pure function of table + geometry state per tick, no new lanes.
  • Observability: characterVoxelUnsticks scratch counter + mantle:count:characterVoxelUnsticks profiler lane.
  • Regression coverage: distilled wedge scenes lifted from the reproducing fuzz (mantle/character/__tests__/embed-wedge.test.ts — sweep wedge, chimney unstick, lateral-pocket unstick, entombed no-ratchet), end-to-end voxel-edit + CC walk suite over the real fast-lane grid/rebuild/payload functions (__tests__/mantle-voxel-edit-cc.integration.test.ts), and a deterministic 3-seed × 6000-tick dig/place fuzz with a transient-motion wedge detector (__tests__/mantle-voxel-edit-cc.fuzz.test.ts).
  • Mantle now applies the collider cook's re-centering translation to CDN model colliders (staging field report: the CDN barrel exhibits — dynamic barrel "made up of a mess of colliders" responding unnaturally to hits; scaled static barrel's bottom not colliding at all). buildColliderMeshFromHulls anchors cooked geometry to minY=0 and records a translation + feetOffset on the cook; rapier applies that translation to every hull/trimesh desc, but mantle's resolveEffectiveMesh dropped the field, so every CDN collider attached with its bottom at the body origin — a full feetOffset above the feet. resolveEffectiveMesh now carries translation, and mapMeshColliders applies it (× the baked-scale ratio) as localPos on every convex-hull piece, the static trimesh branch, and the bounds-fallback box (which also double-counted the offset and used combined scale instead of the ratio).
  • Regression coverage: __tests__/mantle-cdn-model-collider-offset.test.ts — scaled static multi-hull world-AABB reaches the model's true bottom, dynamic compound COM sits at the geometric center, static trimesh and bounds-fallback placements pinned.
  • Mantle joints now interpret authored anchors in the engine's feet-local frame (staging field report: the zoo motor carousel spun at a perfect 30°/s while its rider cubes bounced violently and flew off). ObjectAPI anchors are feet-local — the engine-wide feet pivot — but mantle-runtime births bodies with origin at the collider center and passed anchors through raw, silently reinterpreting every authored mantle joint's anchors center-relative, off by each body's feetOffset. For the translation-locked carousel raft that meant a permanent 0.4 m constraint violation: each substep the revolute point-block injected phantom bias velocity that applyLockFlags zeroed before integration, but the contact solver in between saw the raft surface rocketing — rider contacts position-popped with zero net impulse. Fixed with feetAnchorToOrigin() at the single addJoint site in syncMantleJoints; the misleading "(feet pivot)" frame docs in solver/joints.ts corrected. Raw MantleWorld.addJoint callers (harness, goldens) are unaffected — the conversion lives in the runtime bridge.
  • Regression coverage: zoo-mantle-live.test.ts drives the real authored carousel exhibit + script and asserts world anchors coincide (<1 mm; reads 0.4 m without the fix), steady spin within ±1% of 30°/s, and both riders carried with feet pinned to the raft.
  • Known adjacent gap, deliberately not changed here: the rapier bridge has the identical raw anchor pass-through over center-origin bodies. Rapier is the prod default and existing games may be tuned against current pivots — changing its anchor interpretation is a separate product decision.
  • Night-sky quality package on the star-bake branch (jure's "combine that with this work", #inventors 1784237988):
    • shade_hash11 returns to the u32 PCG hash three-TSL emitted (shade/std.ts; the lume port had swapped in the Hoskins float hash). The float form loses the star lattice's y/z stride steps to f32 ULP at seeds ~10⁶ — adjacent cells return bit-identical hashes, which stamped duplicate stars down whole columns (the 5.2.x night-sky "vertical dot-chains", community thread + screenshot). The star-site WGSL (faint-dust bake) and the CPU bright-star baker now share ONE hash with bit-exact CPU/WGSL agreement: 32-bit integer math both sides, and the JS mirrors round their return through f32 (Math.fround) to match the WGSL's f32(word) conversion — including the ~3×10⁻⁸ saturation tail that returns exactly 1.0 — so the old f64-vs-f32 threshold-drift caveat is dead. Verified: JS mirrors vs an independent BigInt u32 reference in-repo (drift-pin test in sky-radiance.test.ts, which also string-pins the shipped WGSL's PCG pipeline); the WGSL half executed on an L4 compute pass by the #9482 adversarial review — 230,006 seeds, 0 mismatches. Scripted-material hash() is back on three-era parity (range note: [0, 1], reaching exactly 1.0 for the same sliver the three era did). Look risk stated honestly: content authored against the float hash during the 5.2.0–5.2.x window (≈6 days) sees its hash() values change; everything older gets its original values back.
    • Star/grain footprints are sized against the PRESENTED pixel, not the render-res one (sky-store.ts background pass scales the fwidth derivative by viewportSize/canvasSize): under TAAU the scene renders at 0.75×–0.55× and upscales, so render-res sizing birthed stars fat and the resolve kept them fat — half of the community "blurry night sky". Outside TAAU the ratio is 1 (exact no-op). The σ floor is now a named dial, STAR_FOOTPRINT_PIXELS (sky-radiance.ts, 1.4 output pixels), wired through the CPU test mirrors so parity tests track it.
    • New scripts/profile-sky-night: real-GPU cost probe for the sky composite (real LUT computes + real night bake + real star-field bake, saturated throughput + pass timestamps + nvidia-smi sampling). Measured on an L4 at 1920×1080 night: sky fragment work 1.27 ms/frame on 5.2.x master → 0.36 ms on this branch (−72%); sky-occluded floor cost identical 0.066 ms both builds.
  • In-world asset placeholder chip copy tells the truth (ledger 1675): "Spawning {name}…" → "Loading {name}…" for every non-failed load state (formatAutoPlaceholderLabel / deriveFallbackLabel in renderer/components/placeholder-metadata.ts — the label lane of the lume collectPlaceholders spawning-bubble derivation, #6673). The chip claimed generation while the engine was merely downloading an already-baked asset: 48h of Magic CDN traffic shows zero 202s and zero generation jobs — every placard pop-in on a cache-cold load (~130 assets/session; 1h redirect cache) is a plain 200/302 download of a write-once baked asset plus GPU KTX2 transcode. The placard site sees only LoadState, which cannot distinguish a genuine 202-class generation job from a download, so all non-failed states read as loading (a rare generation job reading as loading is the acceptable direction; a download claiming generation was the reported bug). Failed copy unchanged ("Couldn't spawn {name}"). Savi's-eye getUnreadySceneAssets classification and the capture settle gates are untouched. The anti-flash constant is renamed to what it does (PLACEHOLDER_ANTI_FLASH_DELAY_MS); behavior identical.
  • Player settings resolved seam: user-player-settings.ts now exposes the MERGED view (player global ⊕ per-game override ⊕ creator policy) to every engine consumer; transport payloads carry families + legacy flat reduceFlashing (dual-write both directions, wire-compatible with old parents forever).
  • The engine consumes accessibility signals for the first time: reduce-motion (OS-seeded or explicit) governs juice-hud flash peaks/spacing, entity flash and outline coalescing, and camera-shake softening at ONE seam — isReduceFlashingEnabled() reads the resolved view.
  • Sound buses run two-lane (legacy flat + families) with a families-authority latch; per-game sound overrides resolve above player-global.
  • Creator tier: engine.playerSettings (defaults + caps per the family policy — accessibility is player-absolute, never cappable) validated by the contract sanitizer; savi-writable via the existing spec-edit verbs + the player-settings skill.
  • Perf tier vocabulary unified (PERF_TIERS in @spawn/player-settings is the one copy; tier-override plumbing reads the resolved perf family).
  • api.getRoomMode() is now truthful in client-simulated hooks (the #inventors dev-only-flag miss, 2026-07-23). The read's backing fact (TomeRoomModeResource) was only ever mirrored into the SERVER world (room-runtime bindRoomModeResource, from SPAWN_ROOM_MODE), so in every context where gameplay scripts actually simulate under client authority — player behaviors, singleplayer authority worlds, place hosts — the resource was absent and the read fell back to "live" even inside dev rooms. Now the door-minted room mode (readSurfaceRoomMode: cf-edge's templated __SPAWN_CONTEXT__.roomMode — the validated /{engineHash}/{variantId}/{roomMode}/{roomId} path segment it keys the container DO on, the only place edge iframes carry the mode — falling back to the iframe URL ?roomMode= param on kiln's local-kernel and legacy lanes) rides worker.mount (the servedVariantId/publicUrl/relaySpecBootstrap lane) and configureMountedRuntime mirrors it into the mounted world's TomeRoomModeResource. One fact, one mirror per realm: server = env leg, client = mount leg; no second derivation anywhere. The strict reader (readRoomModeFromHref, engine/runtime/relay/relay-spec-fetch.ts) reports absence instead of coining a default — an unknown mode leaves the resource unset so the read keeps its "live" fallback, exactly how modeSegmentStorageKey treats unknown modes (the relay spec-bootstrap claim keeps its own separate join rule — a JOIN needs a concrete mode, unknown-everywhere → "dev" — but is now fed the same resolved surface mode, so an edge-lane LIVE relay room's latest spec fetch targets the live spec instead of the draft).
  • Side effect, more-correct class: the dev-room shared-key FYI (ledger #996) hooks the ObjectAPI job() seam on whatever world the script runs on — with client worlds now knowing their mode, a dev-room CLIENT-simulated storage write records the FYI line too (it was silently skipped exactly where most gameplay scripts run). Same once-per-key-per-world-session dedupe, still informational-only.
  • getRoomMode()'s declared return narrows string → "dev" | "live" (the implementation always guaranteed it), and the JSDoc gains the room-not-viewer fact: the answer is the ROOM's — every collaborator in the build room reads "dev", every player in the posted game reads "live" — so dev-only conveniences gate on it instead of on account ids, and flip off at publish with zero per-person wiring.
  • Savi-side teaching: getRoomMode joins the always-on hot set in the generated tome-api prompt (the updateStorage promotion precedent — the verb lived only in the api-reference/publish skills, and at author time Savi asserted no mode/build flag is script-readable and keyed a creator's dev flag to his account id). Prompt budget +196 chars with a dated ledger entry (cap 15_700 → 15_900); reach-vocab drift guard extended to pin the collaborator/room-not-viewer vocabulary.
  • Tests: client-world twins for the behavior-context read (dev/live/absent), strict-vs-join href reader contracts, and the always-on prompt pins.
  • BREAKING: wire splines hang by default — poles are opt-in (jacob's ruling, #inventors 2026-07-25). stringlight/bannerline/powerline runs render as ONE continuous draped strand through their authored points: per-span droop (explicit wireSag stays absolute meters per span; the default now scales with span length) smoothed through drooped midpoints with a centripetal Catmull-Rom, so the strand crests interior suspension points tangent-smooth instead of meeting them as N independent per-segment sags with hard kinks. Ground-line points hang at the exact wire height the pole layout used (wireHeight ?? poleHeight-derived), so adding poles: true never moves the wire line; snapToTerrain re-measures hang height from the terrain under every point. New SplineSpec.poles?: boolean restores the #10824 grounded pole+wire layout verbatim; poleHeight alone no longer opts stringlight/bannerline into poles. Field specimen: the town-plaza fence-of-poles (stringlight, poleHeight: 5.6, spacing: 1.5 = a grounded pole every 1.5 m around the fountain) re-renders as draped strands with zero spec edits. streetlight/bollard/zipline (pole/anchor products) keep their generated supports.
  • Generated ids change on bare runs: one __spline__cord tube per run (was per-span span_N), with bulb_N/banner_N indexed along the whole arc (was bulb_<span>_<n>). A bare powerline drapes wireCount ?? 1 cables as wire_N.
  • Behavior-moved rigid model instances emit TRUE object motion vectors under TAAU (ledger 1592 — the hand-rolled-rig ghost/smear): static-batch instances and character rigid children rode the camera-only velocity lane (the named TODO(lume-velocity) in lume/camera.ts), so the TAAU history blended them against where they WERE — characters built from model parts animated by per-tick transform writes left a motion smear while skinned GLB avatars stayed sharp. The fix is the skinned path's prev-palette pattern applied to the rigid lanes: ModelInstance records carry a prev-model mat4 lane (MODEL_STATIC_INSTANCE_FLOATS 36 → 52; appended, so every existing offset is unchanged), written from per-visual prev-world retention in the models prepare loop (rotated before each recompose, with the character path's first-frame/teleport zero-motion resets, sharing VELOCITY_TELEPORT_SPEED), and the static velocity shader variant blends it through lume_velocity_pair — exactly as skinned geometry does. Horde (batched) instances and scripted-material displacement remain camera-only (their own TODO(lume-velocity) arcs).
  • BREAKING: Authored Tome UI executes in an opaque-origin sandboxed iframe per UI target (game/creator), not the host page (engine/ui/tome/realm/ — realm-manager, typed broker protocol, frame runtime). Host ambience (parent/top, cookies, host globals, host DOM) is unreachable from authored UI by construction.
  • Documented capabilities are realm-local: dialogs via sandbox allow-modals; sensors/clipboard-write/gamepad/autoplay via the frame allow= delegation; localStorage/sessionStorage via the per-game storage shim (unprefixed keys, existing data resolves); audio hatch twin with policy-mirrored bus gains; window.spawn via the closed sdk.call allowlist; external window.open + ?room= navigation via budgeted nav.* brokers.
  • Boundary failures are loud: teaching errors ride ui.log into getLogs; realm boot failure = one bounded ui.realm fault, world stays playable. Pre-existing fault hooks keep byte-identical strings and throttles.
  • Host window.sendAction/sendAxis/dispatchUIEvent globals, dom-host authored half, event-bridge scope stack, and host lifecycle compile/invoke die with the move; engine chrome de-inlines to delegated handlers.
  • Skills/docs realm-word pass (game-ui, creator-ui, audio, drawn-art, leaderboard, types.ts @tomeapi + regenerated TomeAPI.md/prompt/skill blocks). Full impact table: docs/tome-ui-authored-realm-migration-notes.md.
  • Fixed god-mode drags (brush strokes, spline draws, handle drags) dying the instant they start: a press over the Tome UI authored-realm overlay focused that iframe, the game window fired blur, and the pointer transport's blur handler force-released the primary button — every gesture committed one tick in with a lost hold. handleBlur now ignores a blur whose focus moved into a data-tome-realm frame (its pointer stream forwards back into the same transport handlers); focus leaving for another window/tab still releases all buttons.
  • BREAKING: draped wireSag scales with span length (jacob's drape-taste veto on the whole-run drape's first render, #inventors 2026-07-25: "too few lights. too big lights. weirdly draped?"). On draped runs wireSag is meters of droop per 10 m of span (default 0.8) — absolute-meters droop flattened every long crossing into a bar (0.6 m over a 30 m span reads straight). Pole layouts (poles: true, streetlight, zipline) keep absolute meters per pole-to-pole span since generated pole spans are uniform spacing-sized. Supersedes the 5.2.5 draft's "explicit wireSag stays absolute meters per span" line — if that draft re-folds onto a sha containing this change, fold this note into it.
  • Draped spans hang as true parabolas now (droop 4·sag·t·(1−t), exact at the anchors, sampled ~0.5 m) smoothed through the parabola's knots with a centripetal Catmull-Rom — no rubbery overshoot between anchor and midpoint, tight-but-smooth crests over interior suspension points.
  • Stringlight bulbs are small warm blazing points (r=0.035 globes hung under the cord, warm amber emissive at intensity 5) instead of 15 cm near-white orbs — real festoon look, on both the draped and poles: true layouts.
  • BREAKING: stringlight bulb pitch is engine taste, not spacing (jacob's density call on the plaza render, #inventors 2026-07-26: "there should be like 3-5x more lights i think then good"). Bulbs pitch at 0.4 m along the strand — one constant across the draped and poles: true layouts (the pole lane hardcoded 1.6 m before), overridable per run with the new bulbSpacing field. Bare draped runs used to read spacing as bulb pitch, but field specs authored spacing for the pole-era layout (support pitch — jacob's plaza: spacing 1.5, poleHeight 5.6), and that read is exactly what gutted his 47 m runs to 31 sparse bulbs. His plaza re-renders at 118 bulbs per run (3.8×) with zero spec edits.
  • Bulb caps degrade density, never coverage: a run longer than the bulb cap allows (512 per draped run, 64 per pole span) widens its pitch so bulbs still span the whole arc — a strand dark for its last stretch reads broken; a slightly sparser strand just reads longer.