Spawn
spawn / swhat we're building

pinned

start herewhat spawn isfaqfrequently asked questionsthe betthe spawn bet

updates

engine v5.1Connection2 weeksengine v5.0For Real4 weeksengine 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

Connectionengine v5.12 weeksFor Realengine v5.04 weeksAtelierengine 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.1.10

Engine v5.1.10

July 4, 2026

A patch in the Connection line.

what's new

  • 2D games got a big placement upgrade: backgrounds and distant scenery can now be glued to the camera, so they always fill the screen — no more backdrops drifting away from the view, floating set pieces, or stretched skies. Repeating strips tile seamlessly across the whole view, parallax layers slide at their own speeds, and everything holds up at any screen shape from tall phones to ultrawide.
  • Mid-ground scenery (trees, ruins, set dressing between the backdrop and the play area) now sits properly on the ground across its whole parallax drift instead of floating over dips or drowning in hills.
  • onSoundEnd fires again: scripts that sequence dialogue or music off a sound actually finishing (instead of guessing with timers) work in real games, not just single-thread test harnesses.
  • Building a place and walking players into it in the same script now works — and place-travel errors show up in your logs instead of vanishing.
  • On phones, selection handles are now sized for thumbs — big enough to grab, never covering the object — and brush/draw tools have a visible ✕ to exit.
  • Objects you attach to a parent in another place now go where the parent is, deleting a parent cleans up its attached objects everywhere, and Savi gets told when something can't be saved instead of it quietly breaking on the next load.
  • Fixed a bug where one broken sprite could permanently turn off automatic sprite sizing and pixel-accurate collision shapes for every other sprite in the room.
  • Spinning objects now tumble realistically instead of holding a fixed axis — long or flat objects thrown with spin will wobble and flip the way real ones do.
  • Your game has a music system now: api.music.play("cdn/music-….mp3", { fadeMs: 800 }) starts or crossfades the room's soundtrack, stop fades it out, duck dips it under dialogue, now() tells you what's playing and where it is. Layers let you stack stems ({ layer: "drums" }). Players joining mid-song hear the right spot, and editing your UI never restarts the music.
  • api.audio.duration(clip) gives you a clip's real length in seconds (null until it's known) — no more measuring rituals for timed sequences.
  • playSound finally honors bus: ("Voice", "UI", …) so the right volume slider governs each sound, plus priority: for lines that must not be culled.
  • Your game no longer creeps down to its worst-looking quality while you're actively building — loading hitches from edits don't count against your hardware anymore.
  • Games that swap through lots of big 3D models (previews, galleries, dress-up rooms) no longer slowly fill graphics memory until rendering dies.
  • Game controls and menus now stay clear of the iPhone notch and home bar.
  • Editing big games is way snappier: script-heavy worlds apply Savi's changes in a fraction of the time they used to (a 3,000-object world went from ~2 seconds to ~0.3 per edit), and the engine no longer quietly does every edit twice.
  • Saving game data just got a lot safer: the new api.updateStorage reads your data, applies your change, and writes it back without ever overwriting someone else's save that landed in between — if two things touch the same save at once (double-click purchases, two rooms, an autosave racing a reward), both changes survive instead of one silently erasing the other. Perfect for coins, inventories, and gacha-style rewards that must land exactly once.
  • Save and load can no longer falsely report an empty save when storage is briefly unreachable — instead of your save looking blank (and risking a fresh session overwriting your real progress), the game now sees an honest "storage isn't ready yet" error it can retry.
  • Worlds built by scripts that painted tons of individual voxels no longer freeze for many seconds when the room starts — and they stay snappy while you keep editing.
  • Turning terrain off (or switching terrain types) now actually takes — no more ghost settings from the old terrain sticking around, and when a script gets rolled back, you're told everything it would have changed.
  • Music patterns written in standard strudel vocabulary now either just work (.lpq, .distort, .duck) or tell Savi exactly which vibe verb to use — no more cryptic "is not a function" crashes in your soundtrack.
›technical notes
  • 2D camera-attach placement, wave 1 (#7449): new PositionSpec shape feetPosition: { attach: "camera", x?, y?, depth? } — the object lives in the viewer's camera frame while the entity stays vanilla underneath (origin-parked feet anchor; networking, physics-sync, and AOI see an ordinary entity — aoi.ts byte-identical). The renderer resolves placement against the presented camera (post-smoother, post-pixelSnap); size omitted = aspect-preserving frustum fill (crop, never stretch — pinned at 9:16 and 32:9); parallax is rewritten in the camera frame; repeatX renders one viewport-spanning quad with wrapped UV phase; depth clamps into live near/far with a once-per-entity warn naming both numbers. Camera-frame ontology enforced on both sides of every forbidden pair: attach-onto-physics/parent refused, and physics/npc/parenting/api.move() onto attached entities refused with teaching errors (move is a warned no-op). query() filters attached entities on all three scan paths; getProperty("feetPosition") returns the attach form. Rider: one-arg notifyDmOnce(message) — the message is its own dedup key (the natural call shape previously burned the key and sent nothing).
  • 2D wave 2 (#7813): the legacy plate machinery dies (+44/−495) — backdrop clamp groups (BackdropClampGroup, resolveBackdropClampGroups, backdropClampDeltaOf), isBackdropTextureId's clamp-eligibility role, 3-tile repeatX geometry + whole-tile re-centering for world sprites, and the pre-wave-1 orphan camera-attachments.ts. Mid-band world-sprite parallax survives as applyStandaloneParallaxOffset (taught mid pieces keep their drift). Scheduled behavior deltas for legacy content only: filename-clamped backdrop-* plates can out-travel painted coverage again; world-sprite repeatX strips render one tile wide, and bare repeatX (no parallax) now batches.
  • 2d-mode skill (#7811): mid-band footing judgment replaced after zoo falsification — feet at-or-below the deepest floor the piece drifts across (light −0.5 tuck), replacing local-sink, whose footing was only true at one camera position (camera-attach made drifting mids the default case).
  • The worker topology's decoded clip-duration relay is re-landed (ledger #802). #7088 taught the sim worker to record each audio.clipDuration post from the main-thread decoder into the local duration mirror and relay it to the server as engine.soundDuration; the #6891 client-auth merge clobbered that handler six days later, so the message dropped silently — the mirror never filled, the server's mirror stayed seconds:null, and onSoundEnd never fired in the worker topology (which is every production embed; non-worker simulation was removed from the client entry). The switch case, pre-mount buffer, and mount-time flush are restored against the current runtime-worker; the sound-end trust model (server accepts reports only for clips its own playSound referenced, healed by the cooldown-gated re-offer) is unchanged.
  • ObjectAPI script WRITE verbs (setScript / replaceInScript / insertInScript) now reject memory/ paths with a redirect error naming the working tool (str_replace_editor, which writes to savi's Supabase memory store — the PR #7146 funnel). The game spec replicates whole to every player who joins, so a memory/ key in spec.scripts strands savi's private notes on the player-facing wire; the validator now matches the boundary #7146 already drew. Deliberate asymmetries (plan p-624fbe9e): deleteScript still accepts memory/ keys (the stranded-app migration needs that cleanup path), and getScript / listScripts are unchanged (stranded apps stay readable until migrated).
  • New rate-limited server tripwire: tome.spec.memory_scripts_on_wire (warn, at most once per 60s window) fires from tome/spec-sync-server when an outbound spec revision carries memory/ script keys — meta carries key count, UTF-8 byte total of the stranded sources, and the spec revision; app/room identity rides the server logger context. Observation only: zero spec mutation, zero filtering, no replication/signature/wire change.
  • Ledger 796: container log lines now carry the real app id instead of appid:unknown. cf-edge threads the app UUID into container start env as SPAWN_APP_ID — the same boot-immutable ride as SPAWN_ROOM_ID / SPAWN_ENGINE_HASH (#421/#584) — from the iframe ?ctx payload on prewarms and from the variant-credentials Supabase lookup (one embedded select, no extra request) on bare /rooms WS-first boots. The kernel logger already reads SPAWN_APP_ID into its per-line context and DD intake tags, so every line — including the wedge-attribution lines (room.boot.wedged, worker.fatal) that fire before any late identity bind, and lines from old pinned engines — tags app + variant from the first boot line. Per-app container triage in Datadog works now.
  • Ledger 752: in client-owned multiplayer, definePlace(X) → enterPlace(player, X) in one client-simulated invocation no longer silently strands the traveler. The two calls rode different lanes with inverted server ordering (SpecMutations folded at the tick-end drain, rail.enterPlace executed at command dispatch −100); forwarded spec mutations now fold in a new server system at simulation −150 — before command dispatch — and the client flushes the tracked mutation batch onto the socket before EVERY rail.* command at the one enqueue seam all rails share (enqueueClientCommand), so program order holds end to end for emit/transferControl/voxelEdit/intent alike, present and future rails both.
  • addBehavior/removeBehavior on a just-spawned entity whose spec mirror no-opped (spawn into a place the local spec does not carry) now still record the durable op when the pending spawn already left the tracker (e.g. shipped by a mid-wrap rail flush) — previously the ECS got patched and the durable behavior ref was silently lost on reload.
  • unknown_source rail rejections (the spawn(npc); …rail on npc… one-invocation sibling race — the create rides the StateDeltas lane and can land after the Command) now also report into the behavior fault/log stream, attributed to the stamped source, instead of dying as an uncorrelated cmd.err.
  • definePlace/updatePlace/deletePlace/setDefaultPlace outside a persistence tracker now ride the same tracker-less persistence tail as patchEngine/patchRouting: client-auth behaviors forward them source-stamped call-time (previously a bare call never reached the server at all), and the server/singleplayer authority enqueues them for its own drain.
  • Rail command handler throws (e.g. enterPlace() failed: place … does not exist) now report into the behavior fault/log stream — runtime log (visible to getLogs) plus the DM notifier, attributed to the stamped source entity — before the dispatcher's cmd.err reply, which previously died uncorrelated on the client.
  • Ledger 810: god-mode handle gizmos now size from a 56px thumb target with a 44px iOS floor and an object-fraction cap, converting px→world through the existing UiGlobalsResource viewport mirror; the COARSE_HANDLE_SCALE constant is deleted.
  • Ledger 811: armed touch tools get a visible ✕ exit chip that routes the same god:cancel-tool desktop Escape sends.
  • Ledger 776: spawn() with a parent now resolves the child's place from the PARENT's live place instead of the spawner's — a main controller spawning { parent: "skurp" } while skurp lives in sparkmines no longer mints a cross-place parent/child split at birth (entity and persisted row both land in the parent's place). An explicit place that conflicts with the parent's is still honored but warns on the fault channel — it authors a split.
  • Ledger 777: the destroy cascade's row sweep is now doc-global across all three seams (kernel recorder collectSpecSubtreeRowIdsForDestroy, kernel spec-mutation fold, kiln doc-apply) — a child row split into another place leaves the doc WITH its destroyed parent instead of upgrading to a permanent orphan. Parent refs bind by same-place-wins (a ref resolves to a row of that id in the child's own place first), so #184 cross-place duplicate ids never cascade into another place's same-named family. Seeding by the target's ROW id also lets place-namespaced runtime ids (placeId:objectId) collect their bare-id children.
  • Ledger 779: the persist gate (isMutationPersistable) now refuses spawn rows whose parent can never have a row of its own (player/, camera, tome/exec, system/, _ prefixes) — previously the row persisted and reloaded orphaned forever (the one-nameplate-per-player debris class). The live spawn is untouched; the refusal is taught via the normal fault channel instead of silently minting.
  • No data repair: pre-existing split/orphan rows in stored docs are untouched (separate creator-consent call).
  • Ledger 816: a DrawSprite written without a texture (neither the ObjectAPI sprite setter nor spec apply validates the field — e.g. obj.sprite = { opacity: 0.5 } on an entity with no existing sprite ships texture: undefined) made the server-only sprite/metadata-hydration system throw a synchronous TypeError on every pass; under sustained sprite-write pressure the scheduler's 5-retry ladder completed and permanently disabled hydration for the rest of the process — every later sprite in the room lost auto-sizing and hull colliders until the container recycled. Hydration now skips texture-less sprites (nothing to hydrate from) instead of throwing.
  • FatalSystemError's message claimed "server restart required" — false: the runtime worker catches it and the system is simply disabled for the process lifetime. Message and docs now say what actually happens.
  • Mantle: Box3D technique harvest (PR #7741). Adopted: gyroscopic torque via implicit NR-1 solve in the deterministic lane (pure arithmetic + quat rotate helpers, no transcendentals; scratch rebuilt per tick — nothing new outside the snapshot except contact-event prev-overlap sets, which ride the arena per the graduation clause). Rejected with measurements (docs/physics-native/box3d-harvest-notes.md): static-softness ζ=5 and central friction — both breached the heightfield-rest neighbor-sensitivity epsilon; cross-tick warm starting rejected on the snapshot veto. Cross-runtime golden hash chains identical V8↔JSC; 390/390 mantle tests both runtimes.
  • Music API (plan p-9989dd7d): engine-owned music orchestration. New TomeMusicState component on the tome/spec entity (replicate:"always", reset-proof, carried in the reset snapshot — late-joiners receive music state in the same packet as the spec) holding per-layer {clip, volume, loop, startTick, fade, epoch} targets plus a global duck envelope. New ObjectAPI namespace api.music.{play, crossfade, stop, duck, now} (server-realm; bare music.* in run_script) writes it; a new music-client system applies it through the existing AudioTrack/voice pipeline (Music bus — user volume/mute govern structurally). Crossfades, same-clip in-place volume ramps, named layers (parallel stems), tick-anchored positions, and late-join seek are engine-owned; playback state never touches GameSpec asset refs (regression-pinned). Legacy musicShift runtime untouched; per-player musicShift state suppresses the global default layer on that client while active.
  • Late-join seek plumbing lit up: AudioEmitterValue.time → prep now produces startOffset; the renderer's buffered path start-offset bug fixed (source.start(when, offset) — was a delay, not a seek) with loop-length wrap; music layers force the buffered decode lane past the stream heuristic.
  • api.audio.duration(ref) (server) + audio.duration(ref) (client hatch): sync clip length in seconds, null until the fact mirrors (first decode/report; server reads mark the clip referenced so the fact converges). Rides the #7809 clip-duration relay.
  • playSound now honors bus: (named mixer bus, unknown coerces to "SFX") and priority: (0–100 clamp) — previously silently ignored; defaults unchanged when absent.
  • Zoo Audio zone rebuilt on music.* (trigger-volume room music, jukebox pad, genre-fusion stems pad, duck on the voice pad); the hand-rolled ~95-line ui.js director is deleted; a minimal hatch mount-lifecycle fixture remains.
  • audio skill net-shrinks 110→75 lines (7,849→6,778 chars): the taught director pattern is absorbed by the API; hatch teaching survives as the escape hatch.
  • tome.reconcile.parent_missing warns once per row per boot instead of on every applySpec pass (ledger 776 warn-side, #7754 — was ~5.7k DD events/15d plus ~3/min creator-visible runtime-log spam that nudged Savi into re-fixing the same row). A changed parent value earns a fresh warn. The cross-place case gets honest text: when the parent exists as a row in another place, the warn says exactly that and suggests removing the row or moving it to the parent's place, instead of claiming the parent is missing.
  • Quality governor evidence hygiene (ledger 782, #7760): frames inside the compile/load grace window no longer count as shed evidence — the judged window flushes on every grace sample and steady samples push only after the grace check, so post-grace judgment starts clean. Was ~1 phantom rung shed per Savi edit / place-warp: healthy hardware ground to the quality floor over a 5–10 minute build session. A genuinely heavy world still sheds within the sustain (pinned). Dumps gain a session-cumulative governor transition ring (~last 32 shifts, timestamped with triggers) + landing state.
  • Renderer model retain/release + keep-alive eviction (#7617): the model cache's modelEvictionFrames: MAX_SAFE_INTEGER pin dies — visuals lease their LOD-resolved model ids (two lease slots, settled through every LOD transition) and release on detach; models ride the texture two-tier eviction (grace window measured from release, per-device keep-alive byte budgets 192/64/48MB, oldest-released first past budget). disposeModel now disposes GLB-embedded textures — material.dispose() never freed them and they were the dominant leaked bytes. Fixes the Final Abyss WebGPU device-loss class (one preview entity cycling 28 bespoke 20–27MB GLBs → 1113MB texMem → createBuffer failing at 192 bytes → device death). Riders: device-lost latches the worker-silence watchdog (kills the contradictory follow-up report), and renderer-device-lost carries the GPU adapter description.
  • Ledger 809: kiln hosts measure env(safe-area-inset-*) via a probe element and forward the values once per host across the iframe boundary; the kernel publishes --spawn-safe-area-* CSS vars on <html> with an env() fallback (256px clamp, non-finite→0). God-mode overlays, touch controls, and the server-behind banner consume them.
  • Ledger 787: room.server_behind.episode no longer mixes units. The old headline avgTickWorkMs was per PUMP FRAME (tickWorkMsTotal / framesLastSecond) while phases[]/topSystems[] are per tick — and exactly when the log fires, frames coalesce up to maxStepsPerFrame (8) ticks, so the headline read ~7× the attribution (a P3 burned 25 min on a phantom "87% unattributed"). The field is renamed avgFrameWorkMs (maxTickWorkMs → maxFrameWorkMs), and the log now also carries per-tick avgTickMs, the coalescing factor stepsPerFrame, unattributedTickMs (avgTickMs − Σ phases: jobQueue.poll/world.prune/ctx overhead outside phase brackets), and per-phase residualAvgTickMs (phase avg − Σ that phase's systems: commitTick/stager/flush). All computed at log time in buildServerBehindEpisodeLog (server-behind-episode-log.ts) — zero hot-path additions. No in-repo DD config queries the old field names; saved DD queries on avgTickWorkMs/maxTickWorkMs need the new names.
  • Spec→ECS apply cost collapse (plan p-a2af390c, #7853 + #7826): behavior compilation and signatures now cost O(unique script contents), not O(objects × applies) — a content-keyed compile cache with per-dep source-hash revalidation (failures cache too: a broken lib costs one failed compile per unique content instead of one per object per apply), and behavior signatures are murmur3_128 hashes over exactly the old inputs instead of embedded full sources (185.5MB retained signature chars → 2.2MB on a 3,352-object prod spec — the 3GB-container GC spiral). Grown-spec apply: 1,863ms → 298ms; broken-libs apply: 11,137ms → 506ms. onSpawn re-run semantics pinned identical by op-count tests.
  • Server spec pushes apply once (#7826): every push, boot, and reset applied the whole spec twice — the direct apply plus tome/spec-sync-server's echo re-apply on the next tick (DD-attributed at 2,917ms avg / 20,420ms max per echo on 3,352-object prod worlds). The three lanes that tracked raw spec references in TomeSpec now track the applied snapshot (reference identity is the codebase's echo-suppression contract); a FAILED apply deliberately keeps the raw reference so the sync pass retries.
  • New ObjectAPI verb api.updateStorage(key, updater, callback?, { attempts? }) — the engine-owned safe read-modify-write for storage. get → updater(current) → versioned (compare-and-set) write, automatically retried against the fresher value when another writer lands first (the conflict echoes the current head, so a rebase costs no extra read; default 4 attempts). The updater must be pure — it may run multiple times; return undefined to abort without writing. Callback results: { ok: true, value, version, attempts }, { ok: true, aborted: true }, { ok: false, error } with codes storage_conflict_exhausted (carries attempts + currentVersion) and storage_cas_unsupported. Works in behaviors (callback), lifecycle/cron (awaitJob too), and player-context — the loop's legs ride the existing forwarded storage jobs, so the user/<self>/… self-scoping applies unchanged. Custom spec jobs get the same primitive as env.update(key, updater, { attempts? }) and env.getWithVersion(key).
  • Storage vocabulary is now version-aware: storage:get results additively carry version; storage:set accepts baseVersion (absent = last-writer-wins as before; 0 = create-only expect-absent; N = compare-and-set) and returns { ok: true, version }. A stale base fails with error.code: "storage_conflict", retriable: true, and error.details: { currentVersion, currentValue } — the existing if (!result.ok) return script idiom degrades to a safe SKIP, never a clobber. Identical on both storage lanes (job-pool rail and lifecycle/cron direct-SDK rail) and across the forwarded multiplayer wire (details now rides job error payloads).
  • storage:set's ttlSeconds is now actually honored on the job-pool rail (it was typed and accepted but dropped before reaching kiln); an expired document reads as absent.
  • Honesty fixes: a failed in-memory-rail storage read now propagates its error instead of being swallowed into undefined (a failed get must be distinguishable from empty — the ledger 793 pattern, one rail over), and a compare-and-set write that the storage backend cannot honor (deploy-overlap window with a pre-CAS kiln) fails typed as storage_cas_unsupported instead of silently degrading to last-writer-wins.
  • SDK surface (@spawnco/server, ships in this build): documents.getWithVersion(name), documents.set(name, value, { baseVersion?, ttlSeconds? }) → { version } throwing typed DocumentConflictError { currentVersion, currentValue } on 409, and documents.update(name, updater, { attempts? }) — the same CAS loop, awaitable. Requires the kiln documents-funnel deploy (version-aware GET/PUT); against an older kiln the new verbs fail typed rather than lie.
  • Ledger 793: room-scoped job-pool storage now fails honestly instead of lying. In the variant-change window (pool torn down and recreated before SDK identity resolves) — and any time a room pool has no working SDK — storage:get/set/del/list/query/lock/unlock jobs previously fell back to per-thread in-memory storage and reported ok (reads as value: null, writes persisted nowhere), the exact failure shape that defeats creator-written save guards (_saveLoadOk-style flags armed on a blank read, then defaults overwrote the real save). All seven storage surfaces now fail with error.code: "storage_unavailable" and a self-teaching message; the code rides both lanes verbatim (server-local and forwarded multiplayer). Dev/harness/test pools that intentionally run without a storage backend keep the quiet in-memory fallback unchanged (tri-state sdkIdentity discriminator: object = resolved, null = room-scoped unresolved → honest failure, absent = identity-less pool → in-memory by design).
  • Known limitation, declared: a worker spawned in the null-identity window keeps failing loudly until natural churn — proactive identity-refresh respawn is a named follow-up. Field specimen: a_dreamcatcher's Spawn Tactics progression wipes (dump 77cae01a).
  • Ledger 790: the terrain-edit journal is now compacted at the durable-store seams. Accumulated single-cell voxel-set commands fold into dense per-chunk voxel-stamp commands (the existing palette+RLE encoding) on document save and on document load, so the durable payload and every boot rehydrate are O(edited cells, chunk-bounded) instead of O(lifetime edit count). Semantics-preserving: identical replayed voxel state, appliedRevision/lastServerTimestamp untouched, nonzero-state cells keep their single-cell commands, and commands at or before the last region-shaped command keep their exact positions. Old uncompacted documents still load and self-heal (one compacted re-save on first boot).
  • Terrain-edit fold now re-serializes only chunks whose journal changed (per-chunk appliedRevision/lastServerTimestamp reuse against the previous store entry) instead of re-cloning the whole place per edit, and journal serialization dropped its per-command deep clones (commands are immutable engine-wide). The near-cap size check estimates payload bytes instead of JSON.stringify-ing multi-MB documents on the sim thread.
  • Receipt on the prod-shaped 39k-command specimen (364 chunks): boot rehydrate 292ms → 38ms, single-edit fold+save 125ms → 1.1ms, saved document 39,289 commands / 5.2MB → 365 commands / 0.4MB (dev hardware; the prod container amplified the old cost to ~20s freezes).
  • patchTerrain that changes kind now replaces the terrain wholesale instead of deep-merging residue from the old kind (r-56, #7762; class precedent #7734's patchAtmosphere fix) — applied with the shared switchesDiscriminatedKind predicate at all three folds (live ObjectAPI, kernel spec-mutation fold, kiln durable fold), so patchTerrain({ kind: "off" }, "main") on a heightmap yields { kind: "off" } exactly. Same-kind and kindless patches keep deep-merging. (patchCamera has the same latent class — enumerated, deliberately not fixed here.)
  • Transactional run_script rollback now reports everything it discarded — the silent-swallow that actually ate the specimen's kind-switch (an unrelated patchPlayer schema error rolled back the script and the terrain verb vanished without a trace).
  • Ledger 787: terrain-reanchor's per-anchor re-resolve no longer routes through the full creator-facing createObjectAPI().setProperty("feetPosition") dispatch. updateTerrainAnchoredEntity calls the feetPosition writer directly (terrain-height sample + anchor sync + transform write + owned-scatter-field translate) — the dispatch wrapper's pieces (ObjectAPI construction, sprite-warn doctrine, creator-write schema validation, tracker-gated mutation recording) are state-inert on engine re-apply paths. Parity with the old path (component state + pending dirty marks on twin worlds) is pinned by terrain-reanchor-parity.test.ts.
  • The reanchor system additionally samples the composed height BEFORE writing: when a chunk-version bump didn't actually move the ground under the anchor (sculpt touched the chunk but not the anchor's cell), the write is skipped entirely — component writes were value-deduped no-ops anyway, and the one non-deduped piece, the static physics body dispose+rebuild, is exactly what made per-tick field churn cost ~34ms/tick at 119 anchors on prod app 28642021 (server_behind episodes). The version gate itself was already per-chunk (composedFieldVersionAt sums the authored+runtime versions of the one chunk under the anchor), so no new versioning surface was needed.
  • Tome warn-class log lines no longer land in the Datadog error lane. Self-correcting [Tome] warnings (mutation-warn rail — e.g. spawn() properties.parent auto-hoist — plus interpreter spec-normalization/terrain-mark-skip warns and the camera-state function filter) were written via bare console.warn, which is stderr on the server, and the container log tee ships every stderr line at status:error. They now route through tomeLogger.warn (winston status:warn on the server; unchanged console.warn on the client and in worldless tests). Message text is unchanged, so text-grep dashboards keep matching. Genuine failure logs (tomeLogger.error, purchase-refusal warns) are untouched.
  • Vibe strudel-divergence (ledger 805, #7859): chain verbs whose DSP already exists are now honest aliases (.lpq(r) → .resonance(r), .distort/.dist → .drive, .duck(on) → the duck mod), and ~60 real strudel verbs vibe deliberately lacks (chop, vowel, off, sometimes, n, bank, …) throw teaching errors naming the nearest real verb on the existing fault rail — instead of a bare "…lpq is not a function" TypeError (87×/night in prod). A load-time guard refuses any table entry that would shadow a real chain verb; deliberately-divergent verbs (.rev = reverb send, .coarse = semitone transpose) keep vibe semantics, pinned by test.