engine v5.2.31
Engine v5.2.31
August 20, 2026
A patch in the Lume line.
Sixteen fixes in one: desktop clicks work again in click-driven games, big built-out worlds run much smoother while you build, scripted movers stop freezing in long-running multiplayer rooms, long multiplayer music stops dying after a second, riders stay seated instead of running in place, rotations stick while you drag, 2D characters stop flashing white between animations, spawn-storms no longer flood the room, and pause menus can catch the first Esc.
what's new
- Characters stay seated — riding a vehicle no longer forces the walk animation. A character parented to a moving vehicle or platform no longer plays walk/run at the vehicle's speed, and
npc: { locomotion: false }now releases the player avatar's engine gait too, so a seat clip plays alone. - Your character appears even where animations can't bake — static beats invisible. Where the animation service isn't deployed (local dev), characters load as their unanimated base model instead of holding the placeholder orb forever.
- Savi can no longer mistake a frozen view for your live screen — she says when her eyes are stale instead of guessing from old pixels. When she captures a player's screen and that player's tab has silently stopped receiving updates, she now gets told the frame is stale instead of treating old pixels as the live game.
- Buttons and clickables work with the mouse again on desktop. 5.2.30's touch-tap fix was classifying desktop clicks as UI wherever any script-wired or cursor-styled element sat — in click-driven games, every mouse press could read as a UI hit and world clicks went dead. Desktop clicks now classify exactly as they did in 5.2.21, while phones keep all of 5.2.30's touch fixes (script-wired buttons and pads still take the tap over the engine stick).
- Objects keep their rotation while you move them. Rotating an object and then dragging it somewhere no longer snaps the rotation back mid-drag — a background save arriving while your hand is on the object can't undo what you just did.
- Rapid spawn-and-despawn effects now cost the connection nothing while it is catching up. Entities born and destroyed before the room ever heard about them simply never go on the wire — the room's journal stays clean and real updates (movement, commands, host claims) keep flowing at full rate under any spawner load.
- Your game can react when the mouse cursor is freed — pause menus on first Esc now work. The browser eats the Esc that exits mouse capture, so scripts could never see it; now
input.actions.pointerLockLostfires the frame the lock releases andinput.axes.pointerLockedreads 1 while the crosshair owns the aim — no declarations needed. - Your edits always land in YOUR room — never an invisible copy — even right after joining. A background sim session could briefly win control of the area you were standing in (especially during busy building sessions or right after you joined), and Savi's builds would land in a copy of the world you couldn't see. Now a real player standing in an area always outranks the background session, so what Savi builds is what you watch appear.
- Long music tracks in multiplayer rooms no longer cut out after a second — the song keeps playing through world syncs and rematches.
- Worlds with rapid spawn-and-despawn effects no longer flood the connection — everyone's movement stays smooth. Spawners that mint and destroy entities continuously used to fill the room's journal with bookkeeping and could starve real updates (frozen movers, "like I disconnected without disconnecting"); their churn now rides the lightweight restatement lane and dead spawns fold away under pressure.
- 2D characters no longer flash white when switching animations — they hold their pose while the next animation loads. The spinning loading rings now appear only the very first time a sprite paints, when there is nothing else to show.
- Texture scripts no longer break image loading — script-driven textures and image uploads work together. A game whose art is drawn by texture scripts (
scripts/tex-*.js) no longer collects false "FAILED to generate — rename it" verdicts against healthy scripts, and animated script textures stop getting fake frame grids guessed at them. - State handles never go stale — edits after a state change always land. A script can grab its state once and keep using it: reads see every later patch the moment it happens, and writes always reach the world, even right after a respawn-style state reset.
- Big built-out worlds run much smoother while you build. A background cleanup check used to re-scan every object in the world every tick — in decorated worlds that alone could eat most of the frame budget. It now only wakes up when something actually changes hands.
- Scripted movers keep moving in long-running multiplayer worlds. Anything animated with
Math.sin/Math.cosof game time — patrolling animals, bobbing platforms, spinners — used to freeze in place in multiplayer rooms (moving only once every few minutes) once the room clock got large. They move continuously now.
›technical notes
- The a3dc gait shim honors the seat contract (census 449f52a6 — kevin's walk-while-driving).
Animated3DCharacterLocomotionFeature— the gait writer for every default player avatar — measured speed from world position deltas, so a rider parented to a moving hull (whoseWorldFeetPositionhierarchy-solve rewrites from the hull every tick) read as running and got weight-1 walk/run channels rewritten against the script's seat clip; and it never readproperties.npc.locomotion, so the documented seated-character switch worked on npc-loco bodies but not the player's own avatar. Now a parented entity gaits from its LOCAL feet-position delta (a mounted seat is stationary; a walk across a moving deck gaits as a walk — moving platforms and elevators come along free), the world-framePhysicsBodyStateread is skipped while parented, mount/dismount/re-parent frame changes forget prev-pos (no phantom sprint — the frame is the parent id, so a seat swap between two mounts forgets too), andlocomotion: falsereleases the shim's idle/walk/run channels once and stops all of its writes — facing included — until re-enabled. The channel release is the sharedreleaseA3dcLocomotionChannels, one home with the model-write retire path. - The renderer serves the base model where animated variants can't bake (kevin's local-dev orb, thread 1787165428). In an environment with no ardy motion transport, kiln answers every
?animations=cook with the structured config-absent 503 (base-flowARDY_UNCONFIGURED_*, Retry-After 1h) — and the renderer parked the URL on that verdict, so the character never resolved: orb placeholder forever, in an environment where the base model serves fine. The 503 body now carries a machine-readable class (reason: animated_variant_unavailable, kilnARDY_UNCONFIGURED_REASON); the engine's probe readsreasonoff 5xx bodies ontoMagicCdnFetchError.rejectionReasonand replays it through the verdict tombstone (a second consumer of the same URL classifies with zero network); and the renderer's model load, on exactly that class (isAnimatedVariantUnavailable— the structured reason, never the teaching prose), retries the same URL stripped of?animations=with one honest warn line. Scope pinned by negative controls: an ordinary 503 keeps today's ladder, a dead 404 on the base model still fails honestly (one fallback attempt, no loop), and cold-cook 202s (the 2320 family) never trigger the fallback. Prod (variant available) is untouched — the fallback only exists behind a verdict prod never mints. - Client-sight frame answers carry the room-feed freshness stamp (verdict d29f361d — the client-sight EYE-STALE hole). Scene-view already stamps every capture with the room-feed fact (ledger 2266; appliedSpecVersion since 2337), but the client-sight lane discarded it at the capture provider and
ClientSightAnswerhad no field to carry it — so the EYE-STALE belts that protect look/identify could not covercapture_client_frame, and an attached-but-frozen feed answered frozen pixels captioned "THEIR actual screen" with a fresh capturedAtMs. The stamp now rides the whole lane:ClientSightViewportCapture.feed→ the main→worker hop outcome →ClientSightFramePayload.feed, sanitized at the wire door like every other untrusted field (a malformed stamp drops to the old-engine shape, never poisons the frame). The tool side (cf-studio-chat) refuses a stale-stamped frame with theeye_staleverdict — the transport face (feed state ≠ attached, with its age) and the version face (applied spec version lagging the doc head past the fan-out beat) — exactly the gates look and identify already run. Stamp absent (engines predating this) keeps the old behavior byte-identical. - The row-2437 tap rescue is touch-only, and the interactive grammar never adopts a plane (row 2527 — msweep4 verdict 397ec807, ejthebae's Idle Homestead; his own A/B: same head spec, 5.2.21 works / 5.2.30 dead). 2437's five legs widened the pointerdown
overUiverdict geometrically so the touch arbiter yields over authored controls — but the sameoverUibit rides to the mouse lane, where it means "eat the world press" (raw-capturepressMouseButtonIntentreturns on overUi before the ring append; the worker PointerState marks the press UI), and a desktop mouse has no claim bands to yield: the widening bought desktop nothing and killed pointer-only games' click lanes wherever any authored-grammar rect sat. Container-shaped matches made it total: a delegation wrapper (wrapper.onclick = fn), acursor:pointerscaffold, or onMount delegation on the viewport-spanning game mount matched the widened grammar at plane scale, so every desktop press in the game read overUi:true — and the grant cadence then healed such wrappers topointer-events:auto, turning them into native full-screen UI hits that ate the world lane for every pointer type. Three cuts: (1) input-forward's geometric DOWN widening applies topointerType === "touch"only — mouse/pen presses keep the target-based verdict (5.2.21's classification, byte-identical); (2) tap-actions' fall-through adoption is touch-only (the rescue compensates the touch arbiter's theft and user-agent cancel claims — diseases a mouse never catches; script-wired controls still become mouse-clickable through the grant cadence), and the adoption heal skips delegation containers; (3) the grammar/grant walk never adopts a plane — engine UI planes (data-input-capture="ignore") are never authored controls, and the script-wired grant forms (handler properties, inline cursor:pointer, self-opts — the forms 2437 added) grant leaves only: a script-wired element containing other authored controls is a delegation container, its real controls are granted individually, and clicks still bubble to the delegated handler. Attribute-form grants (INTERACTIVE_SELECTORS) keep their since-forever behavior. All of 2437's own pins stay green: touch adoption, the iOS pointercancel verdict, the grant cadence, the arbiter yield over authored touch surfaces, and bare-canvas presses staying the movement stick's. - A spec apply never stomps the spatial keys of a gesture-held entity (iz's rotate-then-move revert, thread 1786720296.462559, round 3). The interpreter's
updateObjectre-asserted authored feetPosition/rotation/scale over a live god-mode drag: a REPLACE apply unconditionally (rotationSpecChanged = replaceRestore || …), a normal apply whenever the incoming row differed from the applied generation. On a singleplayer client every other protection is structurally off at once — the ObjectAPI hold guard fires only on multiplayer SERVER worlds (gesture-hold.ts), the pending-edit overlay and flush-confirm rail arm only in room-runtime (the client is not the persist authority), and spec-sync's local-sim write mask is explicitly!isSingleplayerWorld— so an incoming apply landing inside the move-drag window (GOD_MODE_PERSIST_IDLE_SECONDS = 1 puts the rotate's persist round-trip exactly there) snapped the just-committed rotation back mid-drag; the drag closure re-writes feet every frame, so ONLY rotation visibly reverted. NowupdateObjectconsults the gesture hold (resolveRefusingGodModeGestureHold, gated by ananyGodModeGestureHeldwhole-world probe so gesture-free applies pay one store lookup) before the three authored-transform writes, on every world: the LIVE write is skipped while the hand holds the entity, the doc keeps the incoming value, the release commit persists the hand's pose for the keys the gesture owns, and the gesture's own updateObject calls (spline preview re-loft) pass through thewithGodModeGestureWriterbracket. Pinned byrotate-then-move-apply-stomp.test.ts— replace and non-replace stomp lanes red pre-fix (180→90 and 180→45), with a non-held sibling restoring in the same apply and the post-release replace reset (ledger #296) as controls. - The fresh-key annihilation fold (row 2493, the W5 wall's named follow-up — composes with
spawner-churn-rebirth-lane-and-tomb-fold). The tomb-prefix fold left one standing tomb per dead cycle in the un-acked outbox, so a sustained fresh-key spawner still accumulated unfoldable entries toward the cap.foldOutboxnow ANNIHILATES a whole lifecycle: a CREATE and its own DESTROY both still inside the un-acked window bracket an entity the wire never saw — nothing of it acked, so nothing of it journaled under this client's msgIds that a welcome retransmit could resurrect — and the birth, every intermediate row, and the death fold to NOTHING. Never across an ack boundary: the candidate test requires the entity's oldest in-window row to be its own create (e:{id}:@c), so an acked/departed birth keeps the destroy traveling (the wire saw the birth; it must see the death — the minimal tomb, with intermediates still folding under it). Whole-entry honesty runs to a fixed point: a family annihilates only while every entry carrying one of its rows folds entirely, so a family row stranded in a surviving batch keeps its tomb. Keyless barriers (commands, events, member-avatar rows) still confine everything to one segment — annihilation can never un-birth an entity a command was enqueued beside, and membership truth is structurally untouchable (member rows are keyless by the fold constitution). Rows that never journal never earn restatement promotion either — the flood dies at the source. Named residual: un-acked is not un-sent, so a create journaled with its ack in flight at socket death can lose its annihilated tomb; the orphan is a keyable non-member entity, exactly the class world-sync's liveIds sweep reconciles — the same crashed-author residual motion's restatement contract accepts. - Pointer-lock loss is visible to behaviors (r-19126e01 — iz's Duskfall, savi platform-gap fceba168). The browser exits pointer lock on Escape and swallows the keydown, so the loss never reached gameplay code:
PointerStateResource.isLockedwas engine-internal, raw-capture handledpointerlockchangewithout synthesizing anything script-facing, andsetCamera({pointerLock})was a write-only lever — "first Esc opens the pause menu" was unauthorable (players pressed Esc twice). Two engine-injected primitives now ride the existing input lanes, no new wire vocabulary: thepointerLockedaxis (1 in every sampled frame while the aim is the crosshair — the browser lock, or the touch aim pin; absent unlocked, where the input proxy reads 0) and thepointerLockLostaction (pulses in the first frame sampled after the lock releases — every exit: Esc, releasePointer(), focus loss, cursor-needing UI). Both are injected by input resolution'sbuildFramefrom the same pointer-resource read that already gates pointer coordinates off locked frames, so state, transition, and coordinates can never disagree within a frame; the pulse rides the frame's actions, so client dispatch, the server's permissive action lane, and client-auth resim all see it identically. Uncontrolled samples consume the edge (a loss with nobody to deliver to never synthesizes a stale pulse later).pointerLockedjoinsENGINE_INTERNAL_AXES(mod-alias stripping, undefined-input notifier exemption),pointerLockLostjoinsENGINE_ACTIONS. - The place election's null-rank window is closed: an in-place real player outranks the headless builder shell even while its folded rank is null (ledger 2525 — the 2464-family's #13176 survivor; dump 4edec9f7). The #13176 cure ranked shells behind resident players by comparing rank streaks, so it never fired when the player carried
rank:null— a folded hidden edge whose journaled visible heal was lagging (under the 2524-adjacent journal flood, for minutes) — and the shell's stale streak won the claimant tier unopposed. Exec routing follows the seat, so Savi'srun_scriptlanded on the shell's headless sim: 25-minute shell-held windows, builds into a world the creator could not see, four field incidents in one jam day. NowelectWinnercarries a PERSONHOOD tier between ranked residents and claimants: a real (non-standing-session), host-eligible resident wins the place over every claimant regardless of rank — the rank is a tiebreak among players, never a gate on personhood. The tier is judged by the same connectivity/tick overlays, and the tick gate is the working arbiter for the genuinely parked tab: beats are EPHEMERAL and never journaled, so they stay truthful when the fold lags — an audibly ticking resident holds the seat, one that decays past the 15s liveness window yields the place to the shell exactly as before, and its returning beat re-takes it. The shell still hosts every place EMPTY of real players (#13176's headless-builder case, pinned as the negative control). Room-truth mode closes the same window fold-purely (electTruthWinnerpersonhood tier ranked by m:_ head seq;hasEligibleRealResidentno longer requires a live rank, so an h:_ row assigning the shell over a rank-null resident degrades to the derived seat). Pinned byrelay-place-elect-null-rank-window.test.tsend to end, exec lane included. - Relay music no longer dies on head re-delivery (row 2504 — lazydev's "music got killed in the first second", dump 05485279). Every relay delivery of the
TomeMusicStatehead re-rebaseslayer.startTickinto the receiver's clock (relay-music-lane.ts translateMusicEntry), so a re-delivered anchor (journal restatement, world-sync) or a fresh publish (rematch duck, volume ramp) shifts it by a few ticks of rebase jitter — andmusic-client.ts applyLayerkeyed the layer instance off(epoch, clip, startTick), beyond the component's own documented contract. A shifted anchor read as a new song: the live track retired, a same-clip replacement minted under a new voice key. On the stream lane (clips priced over the 16 MiB decode budget — every long track) the replacement shares the clip's ONE HTMLAudioElement and MediaElementAudioSourceNode, so the old voice's vanished-key teardown paused, rewound, anddisconnect()ed the voice that had just started — ~0.2 s of music, then silence whilemusic.now()(tick-derived) kept saying playing. Two structural halves: (a) instance identity is(clip, epoch)only, honoring music-components.ts's contract — a startTick delta lands as position truth through the existing per-framederiveSeekSecre-stamp, never a retire+mint; (b) stream elements transfer across voice keys the way same-key retrigger already transfers release — only the element's CURRENT owner may pause/rewind it on teardown, and a voice severs only its own edge out of the shared source node (disconnect(node)instead of baredisconnect()). Class bounds: relay multiplayer × stream-lane music; singleplayer never rebases and buffered voices own independent source nodes (their suites unchanged-green). Kin: 2061 (relayAnchor), 2131 (relay free-run ticks), 2183 (the decode-budget size rail). - Fresh-key spawner churn stops re-earning the restatement lane (row 2493 — the idle-zoo journal flood, tide's read after review 12302a2f disproved the juice hypothesis). The earned-restatement detector keys streaks per (entity, component), so a spawner minting short-lived entities at fresh keys re-paid the earn tax on every cycle — 2 journal rows per component per rebirth plus the create and the tomb, forever (~196k journal rows in one idle zoo session; the wall-5 saturation sawtooth that starved place claims and movers). Now a lane that died with a BORN-YOUNG entity (created and deleted inside
RELAY_CHURN_REBIRTH_WINDOW_TICKS, 300 publish ticks) hands off by component name: the next entity CREATED inside the window inherits the earned lane on its first write. Both ends must be newborn — an entity of unknown or old birth re-earns from scratch (one-shot immunity holds for bystanders and organic lives), and only EARNED streaks (≥RELAY_CHURN_PROMOTE_STREAK) hand off, never partials. Durability is unchanged in kind: inherited writes ride the same EPHEMERAL lane whose keyed create carries initial values and whose rest stamp journals final values on quiescence. Harness meter: a steady-state single-component cycle journals 2 rows (create + tomb), down from 4. - The wall-5 fold learns the tomb's own reach (tide's named backstop).
foldOutboxfolded only same-key rows, so fresh-key churn — where no two cycles ever share a key — could never fold and pinned the un-acked outbox at the cap, closing admission on every real journal-bound publish. Now an older keyed row folds under a NEWER tomb whose prefix covers its key: exactly the supersession the relay itself applies when the tomb lands (journal.ts's TEXT-range prefix retire), one hop earlier. The tomb itself NEVER folds (every observer still learns the death; peers that never saw the folded create skip the unknown-id delete by design), keyless barriers still stop the fold, retag carriers still never fold, and a create's scrub prefix deliberately does NOT supersede — folding a despawn under a same-id rebirth would hide the death from live peers holding the first life (create-apply is an upsert, not a reset). Harness meter: 150 fresh-key spawn+despawn cycles under a black-holed uplink leave 177 outbox frames (one tomb per death + live world) instead of saturating at 256 with admission closed. - Cold-atlas sprite swaps hold the last-good frame on every swap path — the loading rings render only where there is no prior frame to hold (seele's Ironbloom "flashy white", ravi's tape r-7246111b shape 1). A sprite quad already showing real texels must never route to the >2s pending-texture placeholder (the bright spinning rings,
lume/sprites/shader.ts) when its clip/facing swap points at a still-cooking atlas — with gen-on-demand cooks the mixer multiplies the exposure (4 facings × 5 clips per unit kind). The draw/sprite path already held (the mid-fetch hold inapplyComposedSprite— in every 5.2 engine since the lume port; ledger 1817 upgraded it on 07-28 from holding the texture atom to the WHOLE tuple, and either form keeps the lease bound, so no rings) and is now pinned by a mixer-shaped regression test (resolved-overlay?animated=swap past the anti-flash window: held batch keepsresolvedMix 1, zero placeholder census). The hole wasdraw/sprite-instances: a swarm texture swap re-keyed every member onto a lease-less batch immediately, flashing the rings over quads that were already painted.applyInstanceSpriteSwapnow gives the instances path the same hold — showing-resolved + cold target ⇒ keep the ENTIRE last-good draw value, subscribe once, apply the latest driver value whole when the texture lands; a swap reverted while cooking abandons its subscription so the late arrival can't flip the swarm. First paints never enter the hold (no lease to hold), so the loading affordance and the 2sSPRITE_PLACEHOLDER_ANTI_FLASH_MSwindow are byte-identical for genuinely new quads (sprites-store.test.tsanti-flash test unchanged). - Texture-script refs never hydrate as sprite atlases (ledger 2520 — Route Zero, bran's image-load filing r-86d3cafc). A
scripts/tex-*.jsref's frame/fps/clip truth is the script's own sheet declaration, pushed by the renderer bake (assets.scriptedSpriteAtlas) — there is no file to probe, so every probe fired at one could only fail, and its terminal report taught "regenerate under a different name", which mints a fresh scripts/ name that fails again (Route Zero climbed to ~60 dead names following that advice). The sim's atlas hydration door (SpriteAnimationFeature.ensureSpriteAtlasHydration— the intent lane behindsprite.animation, the frame-event dispatch, and both mixer lanes) now carries the sameisTextureScriptRefguard its two siblings already had (sprite-metadata-hydration, the renderer worker's hydration door — the latter pinned by test, unchanged). The guard also kills the smart-inference fallback faking a 4×4/16-frame grid over baked texels forscripts/x.js?…&animated=refs (every mixer targetUrl composes that shape). BothresolveMixerClipTimingtwins (client feature + getChannel/ObjectAPI) answer the honest nominal fallback for script-ref sprites instead of a fake-grid clock, a null-forever timing wedge, or the getChannel twin's doomed rawassetFetchthat 404'd and latchedfailedAtfor the whole session. The rename advice in the terminal atlas report is now unreachable for scripts/ refs (it remains correct for real magic-cdn names). Latch state was in-memory per worker session, so already-latched rows clear on the next reload/engine pin — no manual clear needed. - State handles are forever-live — one entity, one proxy (ravi's kitty read, report 92a51b07 §3). The tracked-state membrane cached its proxy BY STORED BAG, so every whole-bag replacement — patchState's merge (state-merge.ts mints a new object), replaceState/setState, the changes-nothing gate's neighbors, replication ingest — silently froze every handle a script already held: reads served pre-patch values forever, writes no-opped (the old markUpdated closure checked
current === state). Savi's Let's Go Kitty double-death was this bug (the revive patch's owns.invTread answered 0 on the very tick the mercy window was written; 73 edits never caught it), and the corpus models theconst s = getState()top-grab 38 times. Now the cache is keyed PER ENTITY and the root proxy's traps resolve the CURRENT stored bag at trap time (a store-writeVersion stamp keeps read loops on a one-integer-compare fast path; overlay worlds resolve honestly per trap since staged writes never move the base store). Nested writes into containers that survive a merge also replicate now (the mark is no longer gated on the dead root bag). Terminal edge preserved: a despawned entity's handle freezes on its last bag. Bench (interleaved A/B medians,TRACKED_STATE_BENCH=1): getState() 179→159 ns (−11%), root write 200→192 ns (−4%), root read 70.7→74.6 ns (+3.8ns — the stamp check), nested read 115→125 ns. Pinned bytracked-state-forever-live.test.ts(double-death repro, replaceState/ingest faces, suppression-gate and identity negative controls). - Client-realm scope reap is edge-driven instead of an every-tick world sweep (row 2537, leo's 470ms/tick sim).
tome/client-realm-scope-reapused to walk every(TomeRealm, TomeSpawnedBy)row per tick and ask the authority-verdict cache — which clears on every epoch bump, i.e. on ANY spawn/despawn — so a 13k-entity world under active building cold-rewalked the whole ownership graph every tick (470ms of a 650-976ms sim). Scope exit has exactly three edges (wake handback, host revoke, owner AOI exit) plus birth, and every one is observable at write time: candidates now index themselves under every id their verdict walks through (spawner chain + link controllers), coordination-table writes (TomeSuspendedSessions/TomePlaceHosts/TomeSimLeases) re-judge all candidates, a despawn/spawn or authority-component write on a chain key re-judges exactly its dependents, and the non-component inputs (control set, local player/session, hosted-places identity) are O(1) compares per tick. clientSpawn materializations and god-mode tooling (directtome/client-spawnroot — a depth-1 permanent in-scope verdict) never enter the index at all. Verdicts are unchanged: dirty candidates are judged by the sameisClientAuthLocallySimulatedEntity, so the ORBIT-17 orphan class stays dead. Bench (scripts/bench-scope-reap.ts, 10k static decor + 32 candidates under per-tick spawn/despawn churn): mean 8.20ms → 0.004ms per tick; at 13k, 10.74ms → 0.003ms. - Verdict-close redial backoff (dig 31760db2 — the leo-class wedge loop's join-rate lever). The authenticated attach resets
reconnectAttempts(the ledger-716 budget reset), so a session that attached and then died to one of the client's own self-heal verdicts — 4435 silence-watchdog, 4439 redial-fresh/ingress-overflow, 4440 upload ack-silence — redialed at the flat 1s base delay forever; the A1 escalation (ledger 2198) covers only pre-attach bootstrap failures.ClientRoomRuntimenow keeps a short-lived-session streak: an authenticated session that dies to a verdict close while younger thanVERDICT_CLOSE_SHORT_SESSION_WINDOW_MS(30s; the dig's wedge cycles measured openForMs≈11s) increments it, andscheduleReconnectfloors the ladder's exponent on the streak with failRelayJoin's exact arithmetic (base 1000ms × 1.5^streak, capped atRELAY_JOIN_FAILURE_BACKOFF_ATTEMPT_CAP≈ 25.6s, ±20% jitter) — downstream of the attach reset and the watchdog's fresh-budget reset, so neither flattens a wedge's cadence. A session that survives past the window resets the streak; a new room starts clean. Healthy paths are untouched: a one-off half-open condemn is a streak of 1, exponent 0, base delay. Expected effect under a wedge: the redial cadence walks 1s → 25.6s instead of holding flat — ~10-25x fewer joins against the same wedged room. - The sandbox Math shim never quantizes ARGUMENTS — results-only f32 (tide's dig + fix
92a40e5bde; ledger 2317/2319's third face: the relay pen freeze, thread 1787035501).Math.sin(t * speed)withtfrom the relay tier's epoch-anchored room tick puts the argument near 2^35, where f32's ulp is 4096 radians — the old quantize-the-argument wrapper made sin a piecewise-constant plateau (~205s per value at speed 20). The script then wrote the SAME value every tick, same-value ECS writes are silent no-ops, and the zoo's kinematic movers froze on every client, twitching once per ulp crossing (witness v3 bit-exact closure: run-2 held y=2.974238872528076, stepped to 2.5244932174682617 at scriptTick 53615760384 — the model reproduces both bit-exactly, step time +48.2s predicted vs +47.96s measured). Arguments are f64 bit-identical cross-realm already (exact literals, correctly rounded native arithmetic, f32 replicated state, f32 shim outputs), so argument quantization bought zero determinism. Results of implementation-approximated members keep their f32 quantization; abs/sign/sqrt/max/min join the spec-exact native family floor/ceil/round/trunc already ride (ledger 2317).