Skip to content

1.54.3 — stability patch (54-int) - #522

Merged
AlexZ005 merged 49 commits into
mainfrom
feat/54-int
Nov 3, 2026
Merged

AlexZ005 merged 49 commits into
mainfrom
feat/54-int

Conversation

@AlexZ005

@AlexZ005 AlexZ005 commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

Stability patch 1.54.3 on v1.54.2 (integrator 54-int). Union of:

Each fix has a counterfactual test (red on the base, green after). Gates on 4822406: vitest 3717/3717, svelte-check 278/39 = baseline, check:tokens clean, build rc=0. Preview: https://preview-1-54-3.theprototype.pages.dev. Full battery (GPU queue) + release proof follow; CHANGELOG 1.54.3 is in.

🤖 Generated with Claude Code

AlexZ005 and others added 30 commits October 27, 2026 04:47
… chip only

A pinch while measuring showed its own toast on top of the shared
controllerRequired chip. Route it through controllerRequired('measure', slot)
and drop the toast; vr-measure-iwer H asserts the chip at that hand and no toast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading import.meta.env whole made vite inline every VITE_ key (CSP script-src,
peer host, ...) into three chunks, and an exported game zip carried them.
Literal key reads in errorReports, perf beacon, meshy, playOrigin, cloudPlugin.
export-zip now greps the zip's HTML for an injected beacon TAG (not the URL)
and fails on any inlined env object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- one job took 53-73 min against its 75 min limit; release.yml's run for
  1.45.0 timed out with three perf-budget runs started by one tag push
- the push / PR / release.yml triggers now run the --part 1/3..3/3 matrix
  deps.yml already used (~20-25 min a part); deps.yml's per-part calls are
  unchanged; per-part limit 50 min
- no required check names the job (only deps-gate / pc-battery on deps)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… ran

Scene's click rule (moved <= 5 px and pointerdown -> pointerup under 400 ms) read Date.now()
inside both handlers, so on a loaded page (the unfolded N6 under software GL, a gizmo grab on a
big scene) a slow frame or a heavy pointerdown handler between the two made a quick finger tap
read as a long press and the select / deselect was dropped. Both ends and the double-click
window now use the input's own timestamp (performance.timeOrigin + event.timeStamp, still on
the Date.now() clock so VR picks compare against the same epoch). select-41's G14 recorder
logs both clocks beside the gizmo state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…80 s (54 X10)

Since the scripts moved into the sandbox guest, globalThis.__slowRuns was the
guest's global, so the page always read 1; and { timeout: 180000 } sat in the
arg slot of waitForFunction, so the wait ended at the 30 s default. On a
~4 fps runner the 30 slow runs plus the runaway node's per-frame loop limit
take longer than that: red on CI since 1.44.0. The counter now rides the
target box's position.z, the options are the third argument, the runaway
node is removed before the section, and the label carries runs + seconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… out of quarantine (54 X11)

The third peer's "C received the scene" read C's count after a fixed wait and
was red on CI and under load with C partway (1/3/4 of 5). It now waits for A
to have received C's getobjects (premise), then polls up to 90 s comparing C
with A's current count; a red carries the C/A trail over time. A's five boxes
are polled instead of a 1 s wait, and "C holds open conns" is bounded at 60 s.
On a GPU host the trail shows C at 0/5 and 4/5 for up to 12 s before 5/5
(3/3 green), the window a fixed read raced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inger up

A busy page dispatches a 60 ms tap 600 ms late: Input.dispatchTouchEvent sends touchEnd
600 ms after touchStart, stamped t0 + 60 ms (t0 from the browser's clock, since a pc slot's
browser is another machine). A busy loop inside the page cannot stage this, because CDP acks
touchStart only once the page is idle. A premise check asserts the handlers ran > 450 ms apart
while the input says < 400 ms.

Counterfactual: red 2/2 on v1.45.1 (handlers ~633 ms apart, the tap is dropped), select-41
58/0 x2 on the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The four unfolded-N6 checks were quarantined for a tap voided by a long frame between its
pointerdown and pointerup handlers (0cfd251 times it by the events now), and for the tap that
lands on the seated gizmo and leaves the box selected. Green in 6 of 6 runs on the fix: pc x4
and Deck local x2, 58/0 each. The new long-frame check (dfe82e2) is red 2/2 on v1.45.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s window, out of quarantine

Traced under load ~20: Accept -> the 'Backup before' save took ~10 s and the
replicated clear landed ~15 s after Accept, so the 15 s window read a running
load as 'nothing happened'. Removes the quarantine entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Quest error 'Cannot read properties of null (reading getViewerPose)'
came from @threlte/xr's headset task calling xr.getFrame().getViewerPose()
when three has no frame (after the session ended). A small vite transform
(scripts/guard-threlte-xr.cjs) adds the null guard there and in the
hit-test task; @threlte/xr is served as source so dev gets it too. The
app's own XR frame callbacks were already guarded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unhandled rejection with a non-Error reason used to reach a report as
String(reason) - the opaque 'Ma' from iPad Chrome. describeReason (pure,
errorReportCore) now says 'Non-Error rejection (<constructor>): <json>'
with a depth-2, key-capped, 140-char JSON whose long strings are dropped
and which still goes through the message scrub (urls, emails, ids, quoted
phrases). An Error with no stack leads with its constructor name. The
window error seam and the diagnostics ring use the same description.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cancelPull freed the buffers and failed the row, but never told the sender,
and a file that started (or a single-shot answer that came) after the cancel
opened a fresh transfer and landed in the Library anyway. Cancel now asks
every peer it asked to stop (volcancel -> stopSendingTo, the session-volume
Cancel's message) and refuses a late start, a late whole file and a last
slice still being checked, until a new ask or 45 s.

Unit libraryPullCancel: the real pull path over a stalling fake transport
(4 of 6 red on base).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
App.svelte probes for a WebGL 2 context before mounting threlte's Canvas
(the probe context is released at once). Without one it shows
NoWebGLScreen (what to try: hardware acceleration, update the browser,
another browser; Help link to the docs; Reload) instead of a blank page,
and sends ONE 'webgl' report. The reporter files every WebGL create
failure (three's wording, the browser's, the probe's) under one message
and a frame-free signature, so the window error + rejection pair the old
boot produced dedupes to one. e2e no-webgl (added to the battery).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The roadmap-78 repro (limit 30, start, pause 6 s, resume => lost "Out of
time") was not kit.round's clock: the research run's own data shows the
round ended exactly 10.0 s of wall time after go, the length of the
starter Countdown behaviour the lab scene still held. Its this.after()
timers ran on the session clock straight through the pause and called
kit.round.lose('Out of time').

Behaviour timers (method and closure) now count play time: while the
game singleton (or kit.round) is paused they hold, and on resume every
pending timer moves on by the pause's length (replicated, so a handover
agrees); a timer set during the pause counts from the resume. The host
seam is pausedSince(), shared by the app and the logic sim.

Tests: sim/behaviourPause (red on base), kitRoundPauseApp (the round's
own clock on the app runtime path), e2e round-pause-behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The nodesync join hold (fix-agenttest 5) was timed from a peer's FIRST nodesync. A peer
whose graphs were empty at the join sends no snapshot and no nodesync (count 0 is never
broadcast), so its first nodesync came minutes later with its first object graph, and
that real change was held 15 s as if the join were still on; with two more 10 s rounds
the graph reached the peer after ~26 s (measured on a cloud build). The host side never
asks a joiner for graphs at all, so it had no snapshot to wait for in the first place.

noteNodeSyncHandshake starts the window when the link opens (sendHandshake) and again at
the handshake's getnodes (a share-or-stash decision can defer it); a nodesync that beats
both still starts it, so a join is still not drift.

Measured on a cloud build (VITE_CLOUD_PLUGIN, serve-build), first store-only object
graph host->peer: 26.5 s before, 9.1 s after (now bounded by the 10 s nodesync round).
timeline suite on the cloud build: ALL PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A taken sleeve ghost now starts at ghost size on the taking hand and grows to
true size (times the world's scale) as that hand moves 6 to 40 cm from the
sleeve wrist, pushed out along the hand's ray so it never covers more than
about 50 degrees of the view. Full size latches with a detent pulse; after the
latch the grip-move sticks drive it through grabStickAdjust/axesForSlot (Y
reels, X scales). Back within 10 cm of the wrist (or a release before the
latch) cancels: the preview shrinks and flies back into its slot (instant under
reduced motion). Placement converts the preview's world size into objectsGroup
scale, so a miniature world gets a true-size object. Grip with the other hand
at a slot (< 4 cm) is a direct take, claimed before any grab or the V5 world
grab; trigger/pinch on the ray is a distance take, and a pinch at a slot takes
too. A long pinch a hook claimed no longer toggles the radial menu on release.

Pure maths in vrSleeveMath.js (unit tested); the frame path reuses scratch
vectors. New IWER suite vr-sleeve-iwer (grip/trigger/hands, miniature world).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ide screen

On the OPPO Find N6 unfolded (770 px) a docked Connect bar pushes the top-right cluster (notes, bell,
people, profile circle) down to 62..114. 76-int cleared only the profile circle and only for the AI
window, so its header still sat 2 px under the bell row, and the Debugger and every module toolbox
(top 76) opened with their headers under the cluster. dragWindow's top floor is now ONE rule for
every window: windowSize.TOP_CHROME (Connect pill/bar, #hud-tr, #avatar-trigger) through the pure
topChromeFloor, which replaces the Connect-pill check and the AI window's clearAvatar opt-in.

Tests: tests/unit/topChromeFloor.test.js; connector-window N6 checks that no window header (AI window,
Debugger) sits under the top chrome (red on base: "ai-assistant-window top 112 under #hud-tr").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n-in button

Signed out with a provider of your own, the AI window's header carries "Sign in to connect" (76 FW-AI);
on the folded N6 sheet (390 px) that cut the title to "AI assist…". The header now measures its own
width (container query) and below 440 px the button reads "Sign in"; its accessible name and tooltip
stay "Sign in to connect".

Test: connector-window N6 checks the header title is not cut short and the button keeps its name
(red on base: folded title scrollWidth > clientWidth).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tab open across a redeploy (or a dropped fetch on a phone) failed lazy
imports silently. A build-only vite transform (scripts/import-retry.cjs)
splices one retry into vite's preload helper, which every lazy import
goes through; src/lib/chunkReload.js does the retry (400 ms) and, when
it fails too or a chunk failure reaches vite:preloadError / an unhandled
rejection, shows ONE sticky 'A new version is ready - Reload' toast. It
never reloads by itself (no loop). Error reports file every chunk
failure under one message and a frame-free signature. e2e stale-chunk
on a built app (battery: SKIPs on a dev server).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ee about the authority

1.48.1 sent a shared Set Variable to one writer, the kit authority, which
applies each event (rid) once. But the authority is each peer's own reading
(physics initiator, kit document writer, open links): while two peers read it
differently - a stale initiator after a VR peer's run, a roster or mesh not
caught up - each sent the press to a different peer, both applied it, and the
second applied it on top of the first one's write: Add 1 counted twice and a
Set t = NOT Get t toggle flipped back, while a set-to-a-value button looked
fine. The rids now travel in the game singleton (last 32, unioned on merge),
and an event whose rid is in the state is never applied again by anyone.

Unit gameVarOp (4 new, red on base). e2e toggle-peers: 2 peers, 2 peers that
disagree about the authority, then 3 peers, a press on every peer (On Click
NOT-toggle, HUD button NOT-toggle, Add 1, Latch value): red on base in the
disagreeing rounds (t back to 0, k +2), all green after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and unfolded

The welcome (Connect Claude + your own provider) was ~560 px of content in a 440 px window (a half-height
sheet on the folded N6), so the own-provider card sat below the fold inside an inner scroll. While the
welcome shows, the floating window takes its content height (capped by the room below its top, so it
still scrolls only when the screen truly is too short), and on a phone the AI sheet asks for the full
detent through the new phoneShell.phoneFirstDetents (local, never saved) — a height the person chose
still wins.

Tests: connector-window N6 (folded + unfolded + unfolded light) checks the welcome has no inner scroll
and the own-provider card is inside the window (red on base: scroll 561 > 325 folded, 470 > 321 unfolded);
tests/unit/phoneFirstDetent.test.js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot reproduce)

Two peers; a painting made the agent's way (Plane + a library image as map)
deleted by hand and by the AI tools' delete_objects on either peer, and at
agent speed (made, textured and deleted in one go). After each, on both
peers: out of the scene graph, no mesh still drawing its picture, and the
receiving peer disposed the texture. All green on the 54-int base, so the
owner's take (1.51, VR and PC) is not reproduced; the suite stays as the
regression proof.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AlexZ005 and others added 19 commits November 3, 2026 00:43
…rom the add menu

At a real frame rate (node-ux-keys on a GPU), Shift+A → Enter → Space → Enter added two nodes and left
focus in an OLD node's number field, so M typed "m" instead of muting (and H, Q, E, arrows, ? after it).
rememberFocus treated the first menu's filter input — the newest, vanished focus-trail entry, under 3 s
old — as a vanished opener and walked the trail back to the last field you had clicked. A vanished
opener is now never a typing field, and the trail fallback never lands in one (keyScope.isTypingField,
one rule): focus moves into a field only when you press it.

The "Something went wrong: ResizeObserver loop completed" toast: ContextMenu re-placed itself INSIDE its
ResizeObserver callback, and once you type the search list caps its own max-height there — the box
resized in its own callback, leaving minimalScroll's observer on the same box undelivered (traced by
wrapping ResizeObserver per observer: ContextMenu place + minimalScroll on .ctx-scroll). The re-place
now runs on the next frame.

Tests: tests/unit/rememberFocusTyping.test.js (red on base); node-ux-keys gains "keyboard not in a text
field after adding" + "no ResizeObserver loop" (red on base at GPU: focus in n1's field, 2 loops; green on
the fix with GPU and with SwiftShader). Green at a real frame rate now, so it leaves battery desk.software.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phones and the Quest browser have no drag and drop, so the Explorer now offers
"Add files…" (a real file picker) in the background menu, a folder's menu, a
mount's menu and a mounted folder's menu, plus a button on the empty view. The
picked files take the SAME route as a drop: onDrop's body became
routeIncomingFiles(files, folder), shared by both (mounts buffer, packs take
zips, scene views refuse, duplicates ask). "New file ▸ Script / Note / JSON /
Text" creates a text file from the Code tab's codeFilesCore templates (a Text
template joins them) through codeFiles.createCodeFile, disabled for viewers.
A button inside the grid keeps its own Enter/Space (the grid's Enter used to
swallow it). New suite explorer-add-files (desktop + phone, dark + light).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Possess drove an object with tank controls and had no way off the
ground. Space or a gamepad's A now hops the possessed object: a
kinematic hop (possessJump.js, a leaf) that reaches Jump height
(default 1.2 m, 0 = off) under the scene's gravity and lands exactly
on the take-off height; one hop at a time, the press is an edge. The
landing pose is always sent to peers. Possess also reads the gamepad's
left stick now (it read only keys and the VR stick). VR is not wired:
possess releases on entering VR.

The Possess Object node gets a Jump height param, and its node doc and
the SDK doc list the controls.

Tests: unit possessJump; e2e possess (Space -> the body leaves the
ground and lands).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reproduced on the real applyWander (what flowRuntime runs per frame): a
group placed at y 0 with the node's defaults bobs within ry 0.3 (y -0.19
at t 0 rising to +0.05 at t 10 s), and a 0.2 m ground slab wired as the
area pulls the wanderer to the slab's middle (y -0.1, half sunk).

A new "ground" toggle keeps the height the object was placed at while
x/z still wander, with or without an area. New Wander nodes start with
it on; a saved node without the key keeps its 3-D drift (the aquarium
fish). The node doc says when to switch it off.

Test: unit wanderGround (3 of 4 red on base).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he arch

New check 3.6b: after Start, the module's spawn must be more than
1.7 m (the plinth's 1.4 m half-width + 0.3) from the entrance plinth's
centre. Red on the published scene (0.00 m: the arch was centred on the
spawn), green on the scenes fix (scenes c56e3e6 on
fix/54i-dungeon-spawn, 2.50 m). Point the suite at a scenes worktree
with DUNGEON_REALMS_TPSCENE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nish)

three bakes renderer.shadowMap.enabled into each program and does not re-check it per
draw. setShadowMapEnabled is now the one writer (lightParams + environment) and marks
every material for update on a flip. Suspect from the play sub-agent's diag7-9: member
floor clones warmed while kit-instancing turned shadows off stayed missing from the
canvas until material.needsUpdate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(row u, cause not proven)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lines the env object

1.54's funnel.js used import.meta.env as a value, which made vite inline the whole env
object again (export-zip red on the 1.54.3 preview: 'VITE_APP_ORIGIN:' in the zip's code).
A unit guard now fails on any bare import.meta.env in src/ (red on the old line).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… catalog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ers; state_unsafe_mutation in session-volume)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… accepts a window held below the top chrome on a tablet-wide screen (54 f); sheet-handoff tests hand-off only on a list long enough to absorb it (Explorer empty state + Add files overflows a short sheet by ~40 px)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant