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 }) runsupdate()on that cadence, andnear: { tag: "player", radius: 50 }lets far-away objects sleep until someone gets close. The engine skips the skipped ticks entirely — no more hand-rolledtick % Ngates — andapi.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, wherefetchis 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: trueon 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
bulbSpacingif 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.jobsauthoring door (#9887); cron and lifecycle jobs survive the dispatch gate and room restarts (#9867);rail.purchaseactor 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.wangscripted 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:
compileFunctionFactorymemo 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.- 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. - The 3,021-char
_f32+ Math-shim preamble inlined per compile unit becomes a two-line bridge toglobalThis.__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. scriptSourceHashCache+extractDependenciesCache(content-keyed) clear oninvalidateBehaviorCache/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 } | [...] }, andtome/behavior-updateevaluates 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.everyphases deterministically per entity (djb2 of the entity id — no mutable counters, identical on every simulating side);nearsnapshots 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.TomeBehaviorUpdateDebugResourcegainsscheduleSkippedEvery/scheduleSkippedNear.- Composed
updateis presence-gated likeonNoise:composeBehaviorsno longer synthesizes an unconditional update wrapper, so behavior-update'sif (!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.lostnever fires and everycreateRenderPipelineAsyncrejects 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_scenefailed generically on every call, and neither the player nor Savi was told the device was gone. The instance-dropped class now joinsGPU_DEVICE_DEAD_RE, andhandlePipelineBuildErrorfeeds 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 (…)") andview_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. Therenderer-device-lostdiagnostic'sdatagainsadapter/adapterDegraded/adapterIntegratedHintplus exactgpuBuffers/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 lostconsole 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 withglobalOutbound: null, so the documented webhook pattern (fetch(args.url)in aspec.jobshandler) was structurally dead on the room server. Job scripts now compile with a fetch shim that forwards a bounded request descriptor over the existingJOB_IOforward lane, and the room host executes the egress — mediated, never a blanket unlock (tome/jobs/job-fetch.tsowns the policy;RoomJobIo.fetchExternalis 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
deadlineMsstill 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.tsjob_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.inspectcomponent allowlist + typed-array summary tokens (p-300376ee lever 5, C1): the debug snapshot accepts an additivecomponents?: string[]filter; the headless-room-host 2s poll passes exactly the five component names itsbuildWorldViewreads (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.25MBtome/specvalue) to KBs.serializeDebugValuesummarizesArrayBufferViews at any depth, so bulkreplicate:"never"typed-array holders (geometry/bespoke class) stay cheap in the unfiltered walk too.- Object diff signatures are compact content hashes (C2e):
TomeObjectDiffSignaturesResourcestored the full canonical-JSON string of every object's properties/tags/state — ~5.6MB resident geometry-as-text per realm on baked-heavy worlds. Nowmurmur3_128(canonical) + ":" + length(~39 chars/field); equality-compare semantics unchanged. - Allocation-free spec-value equality (C3a): the duplicated
specValuesEqualcopies (object-api + interpreter) collapse intosrc/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):
formatLogArgcaps 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_starLayer3×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
nightChangeddirty mark + an exact-density guard. - Costs stated honestly: the per-pixel night path is now 1 index fetch + ≤4 records + grain (the
nightVisibility > 0uniform 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-wgslcompiles every generated sky shader on a real WebGPU device (GPU machine required).
- Dense faint layer → the night equirect bake (sky-night.ts, same dirty gating — its params were already in
- Place-host wedge self-heal is convergence-bounded (ledger 1532, dump 85d9a1cc):
healWedgedHostCycle's sole-candidate leg now spends at mostHOST_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 thehasStreamed=truelegs, 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 newwedgeHealBreakerOpenstenure 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 (workerdws.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/nightand 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.episodelanetome/spec-syncvisiting ~80/111 objects in the field console). The fix extends the execution-authority rule (behavior-update'sisClientAuthLocallySimulatedEntity) to the spec-apply lane:forwardSpecMutationsToRoomSinkrecords 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 trackedTomeSpechead 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 armsmaskedApplyTick, 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 in2d-side, flat in2d-top) and never inherits the entity's rotation as in-plane spin, whatever writes the rotation — NPC steering's yaw-to-travel-heading (npc.turnclamps to min 10°/s, so it can never be zeroed), physics, scripts. Before this, every walkingproperties.npccharacter sprite in2d-toprendered 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 whenuprightis 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 —billboardowns rotation inheritance there. - Spec plumbing:
ObjectProperties.sprite.upright(types.ts) +DrawSpriteValue.upright(carries through sparse merges likebillboard) +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 wantupright: 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 CSSzoom = viewportHeight / referenceHeight(recomputed live on resize), and the worker'swindow.innerWidth/window.innerHeightmirror reports the same reference space (innerHeightreads exactlyreferenceHeight), 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 viaapi.patchEngine({ ui: { referenceHeight } })with no reload. patchEngine's silent-ignore warn lane and the tome-schemasEngineConfigSchemaboth 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=collidervariant resolved against the absolutehttps://spawnfile.iobase, 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, whichgetModelColliderrides) now fetches throughassetFetchtoo instead of ambientfetch— identical in browsers and on the Bun container tier (no route installed ⇒ assetFetch IS fetch, and the container keeps its legitimateSPAWN_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),
fetchMagicCdnCollidernow 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.tsdecodesEXT_meshopt_compressionbufferViews through the vendored wasm decoder (lume/model/meshopt.ts initMeshoptDecoder, injectable per theMeshoptDecoderLikepattern; 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; acollider-cooked-locallyruntime log names the degradation direction once per cook.
- Same rail as the renderer. The lightweight collider lane already fetched root-relative
- 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 insystems/__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.tsVERIFY_SWEEP_OPTIONS, the cast API's existingincludeInitialOverlap: 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.
- 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 (
- Observability:
characterVoxelUnsticksscratch counter +mantle:count:characterVoxelUnsticksprofiler 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).
buildColliderMeshFromHullsanchors cooked geometry to minY=0 and records atranslation+feetOffseton the cook; rapier applies that translation to every hull/trimesh desc, but mantle'sresolveEffectiveMeshdropped the field, so every CDN collider attached with its bottom at the body origin — a full feetOffset above the feet.resolveEffectiveMeshnow carriestranslation, andmapMeshCollidersapplies it (× the baked-scale ratio) aslocalPoson 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-runtimebirths 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'sfeetOffset. 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 thatapplyLockFlagszeroed before integration, but the contact solver in between saw the raft surface rocketing — rider contacts position-popped with zero net impulse. Fixed withfeetAnchorToOrigin()at the singleaddJointsite insyncMantleJoints; the misleading "(feet pivot)" frame docs insolver/joints.tscorrected. RawMantleWorld.addJointcallers (harness, goldens) are unaffected — the conversion lives in the runtime bridge. - Regression coverage:
zoo-mantle-live.test.tsdrives 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_hash11returns 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'sf32(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-materialhash()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 itshash()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/deriveFallbackLabelinrenderer/components/placeholder-metadata.ts— the label lane of the lumecollectPlaceholdersspawning-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 onlyLoadState, 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-eyegetUnreadySceneAssetsclassification 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.tsnow exposes the MERGED view (player global ⊕ per-game override ⊕ creator policy) to every engine consumer; transport payloads carryfamilies+ legacy flatreduceFlashing(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 + theplayer-settingsskill. - Perf tier vocabulary unified (
PERF_TIERSin @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-runtimebindRoomModeResource, fromSPAWN_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) ridesworker.mount(the servedVariantId/publicUrl/relaySpecBootstrap lane) andconfigureMountedRuntimemirrors it into the mounted world'sTomeRoomModeResource. 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 howmodeSegmentStorageKeytreats 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'slatestspec 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 narrowsstring→"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:
getRoomModejoins 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/powerlineruns render as ONE continuous draped strand through their authored points: per-span droop (explicitwireSagstays 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 addingpoles: truenever moves the wire line;snapToTerrainre-measures hang height from the terrain under every point. NewSplineSpec.poles?: booleanrestores the #10824 grounded pole+wire layout verbatim;poleHeightalone 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__cordtube per run (was per-spanspan_N), withbulb_N/banner_Nindexed along the whole arc (wasbulb_<span>_<n>). A bare powerline drapeswireCount ?? 1cables aswire_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)inlume/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:ModelInstancerecords carry a prev-model mat4 lane (MODEL_STATIC_INSTANCE_FLOATS36 → 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, sharingVELOCITY_TELEPORT_SPEED), and the static velocity shader variant blends it throughlume_velocity_pair— exactly as skinned geometry does. Horde (batched) instances and scripted-material displacement remain camera-only (their ownTODO(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 frameallow=delegation;localStorage/sessionStoragevia the per-game storage shim (unprefixed keys, existing data resolves); audio hatch twin with policy-mirrored bus gains;window.spawnvia the closedsdk.callallowlist; externalwindow.open+?room=navigation via budgetednav.*brokers. - Boundary failures are loud: teaching errors ride
ui.loginto getLogs; realm boot failure = one boundedui.realmfault, world stays playable. Pre-existing fault hooks keep byte-identical strings and throttles. - Host
window.sendAction/sendAxis/dispatchUIEventglobals, 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.handleBlurnow ignores a blur whose focus moved into adata-tome-realmframe (its pointer stream forwards back into the same transport handlers); focus leaving for another window/tab still releases all buttons. - BREAKING: draped
wireSagscales 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 runswireSagis 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 uniformspacing-sized. Supersedes the 5.2.5 draft's "explicitwireSagstays 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: truelayouts. - 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 andpoles: truelayouts (the pole lane hardcoded 1.6 m before), overridable per run with the newbulbSpacingfield. Bare draped runs used to readspacingas bulb pitch, but field specs authoredspacingfor 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.