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,stopfades it out,duckdips 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.playSoundfinally honorsbus:("Voice", "UI", …) so the right volume slider governs each sound, pluspriority: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.updateStoragereads 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.tsbyte-identical). The renderer resolves placement against the presented camera (post-smoother, post-pixelSnap);sizeomitted = aspect-preserving frustum fill (crop, never stretch — pinned at 9:16 and 32:9); parallax is rewritten in the camera frame;repeatXrenders one viewport-spanning quad with wrapped UV phase;depthclamps 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-argnotifyDmOnce(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-tilerepeatXgeometry + whole-tile re-centering for world sprites, and the pre-wave-1 orphancamera-attachments.ts. Mid-band world-sprite parallax survives asapplyStandaloneParallaxOffset(taught mid pieces keep their drift). Scheduled behavior deltas for legacy content only: filename-clampedbackdrop-*plates can out-travel painted coverage again; world-spriterepeatXstrips render one tile wide, and barerepeatX(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.clipDurationpost from the main-thread decoder into the local duration mirror and relay it to the server asengine.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 stayedseconds:null, andonSoundEndnever 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 rejectmemory/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 amemory/key inspec.scriptsstrands savi's private notes on the player-facing wire; the validator now matches the boundary #7146 already drew. Deliberate asymmetries (plan p-624fbe9e):deleteScriptstill acceptsmemory/keys (the stranded-app migration needs that cleanup path), andgetScript/listScriptsare 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 fromtome/spec-sync-serverwhen an outbound spec revision carriesmemory/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 asSPAWN_APP_ID— the same boot-immutable ride asSPAWN_ROOM_ID/SPAWN_ENGINE_HASH(#421/#584) — from the iframe?ctxpayload on prewarms and from the variant-credentials Supabase lookup (one embedded select, no extra request) on bare/roomsWS-first boots. The kernel logger already readsSPAWN_APP_IDinto 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.enterPlaceexecuted 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 EVERYrail.*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/removeBehavioron 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_sourcerail rejections (thespawn(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 uncorrelatedcmd.err.definePlace/updatePlace/deletePlace/setDefaultPlaceoutside a persistence tracker now ride the same tracker-less persistence tail aspatchEngine/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'scmd.errreply, 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
UiGlobalsResourceviewport mirror; theCOARSE_HANDLE_SCALEconstant is deleted. - Ledger 811: armed touch tools get a visible ✕ exit chip that routes the same
god:cancel-tooldesktop Escape sends. - Ledger 776:
spawn()with aparentnow resolves the child's place from the PARENT's live place instead of the spawner's — amaincontroller spawning{ parent: "skurp" }while skurp lives insparkminesno longer mints a cross-place parent/child split at birth (entity and persisted row both land in the parent's place). An explicitplacethat 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 whoseparentcan 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
DrawSpritewritten without atexture(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 shipstexture: undefined) made the server-onlysprite/metadata-hydrationsystem 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
TomeMusicStatecomponent on thetome/specentity (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 namespaceapi.music.{play, crossfade, stop, duck, now}(server-realm; baremusic.*in run_script) writes it; a newmusic-clientsystem 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). LegacymusicShiftruntime 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 producesstartOffset; 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,nulluntil the fact mirrors (first decode/report; server reads mark the clip referenced so the fact converges). Rides the #7809 clip-duration relay.playSoundnow honorsbus:(named mixer bus, unknown coerces to "SFX") andpriority:(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_missingwarns 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_INTEGERpin 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).disposeModelnow 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 →createBufferfailing at 192 bytes → device death). Riders: device-lost latches the worker-silence watchdog (kills the contradictory follow-up report), andrenderer-device-lostcarries 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 anenv()fallback (256px clamp, non-finite→0). God-mode overlays, touch controls, and the server-behind banner consume them. - Ledger 787:
room.server_behind.episodeno longer mixes units. The old headlineavgTickWorkMswas per PUMP FRAME (tickWorkMsTotal / framesLastSecond) whilephases[]/topSystems[]are per tick — and exactly when the log fires, frames coalesce up tomaxStepsPerFrame(8) ticks, so the headline read ~7× the attribution (a P3 burned 25 min on a phantom "87% unattributed"). The field is renamedavgFrameWorkMs(maxTickWorkMs→maxFrameWorkMs), and the log now also carries per-tickavgTickMs, the coalescing factorstepsPerFrame,unattributedTickMs(avgTickMs − Σ phases: jobQueue.poll/world.prune/ctx overhead outside phase brackets), and per-phaseresidualAvgTickMs(phase avg − Σ that phase's systems: commitTick/stager/flush). All computed at log time inbuildServerBehindEpisodeLog(server-behind-episode-log.ts) — zero hot-path additions. No in-repo DD config queries the old field names; saved DD queries onavgTickWorkMs/maxTickWorkMsneed 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_128hashes 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 inTomeSpecnow 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; returnundefinedto abort without writing. Callback results:{ ok: true, value, version, attempts },{ ok: true, aborted: true },{ ok: false, error }with codesstorage_conflict_exhausted(carries attempts + currentVersion) andstorage_cas_unsupported. Works in behaviors (callback), lifecycle/cron (awaitJobtoo), and player-context — the loop's legs ride the existing forwarded storage jobs, so theuser/<self>/…self-scoping applies unchanged. Custom spec jobs get the same primitive asenv.update(key, updater, { attempts? })andenv.getWithVersion(key). - Storage vocabulary is now version-aware:
storage:getresults additively carryversion;storage:setacceptsbaseVersion(absent = last-writer-wins as before;0= create-only expect-absent;N= compare-and-set) and returns{ ok: true, version }. A stale base fails witherror.code: "storage_conflict",retriable: true, anderror.details: { currentVersion, currentValue }— the existingif (!result.ok) returnscript 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 (detailsnow rides job error payloads). storage:set'sttlSecondsis 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 asstorage_cas_unsupportedinstead 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 typedDocumentConflictError { currentVersion, currentValue }on 409, anddocuments.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/unlockjobs previously fell back to per-thread in-memory storage and reportedok(reads asvalue: 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 witherror.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-statesdkIdentitydiscriminator: 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-setcommands fold into dense per-chunkvoxel-stampcommands (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/lastServerTimestampuntouched, 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/lastServerTimestampreuse 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 ofJSON.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).
patchTerrainthat changeskindnow 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 sharedswitchesDiscriminatedKindpredicate at all three folds (live ObjectAPI, kernel spec-mutation fold, kiln durable fold), sopatchTerrain({ kind: "off" }, "main")on a heightmap yields{ kind: "off" }exactly. Same-kind and kindless patches keep deep-merging. (patchCamerahas 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.updateTerrainAnchoredEntitycalls 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 byterrain-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_behindepisodes). The version gate itself was already per-chunk (composedFieldVersionAtsums 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.parentauto-hoist — plus interpreter spec-normalization/terrain-mark-skip warns and the camera-state function filter) were written via bareconsole.warn, which is stderr on the server, and the container log tee ships every stderr line at status:error. They now route throughtomeLogger.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.