Skip to content

scatter-sync: room edits reach a joiner whose approval dial-back is still opening; scatter suites back to required - #474

Merged
AlexZ005 merged 78 commits into
feat/60from
fix/scatter-sync
Oct 25, 2026
Merged

AlexZ005 merged 78 commits into
feat/60from
fix/scatter-sync

Conversation

@AlexZ005

@AlexZ005 AlexZ005 commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

On GitHub's runners four two-peer scatter suites (scatter-select / -convert / -edit / -sources, from roadmap 51) failed with FAIL B has the terrain; deps-auto moved them to optional (PR #468).

Cause: a race, plus slowness

Reproduced locally with CDP CPU throttling (new E2E_CPU_THROTTLE) and a wire tap on both pages:

  • The host's approval dial-back opened 15 s after approval (51.5 s after connect). The suite's /create Terrain at 46.8 s went through broadcast, which silently skips a conn that is not open, so the create never reached B.
  • B only converged because its full-state request was answered later, through the slow GLTF reply: the terrain arrived +24.6 s after create, past the suites' 20 s window. Every CI log shows the same thing: "B has the terrain" FAIL, then "B holds the same scatter" PASS.
  • The remaining reds were load: a frame takes 1.5–2.5 s on a two-page SwiftShader runner (E2E_LONGTASKS), so B dispatches late. On the slowest runner, real clicks timed out at 30 s, and no task on the clicking page ran longer than 8 s.

Fix

  • src/lib/dialBackQueue.js (a leaf with unit tests) + peerHandler: room-scoped broadcasts to a peer in approvedDialBacks whose conn is still opening are held, then flushed in order, re-gated (withholdFrom), at the end of sendHandshake. Bounded at 300 messages / 120 s; on overflow the whole queue is dropped, because the full-state reply still covers it. The per-peer gate was extracted from broadcast unchanged.
  • Counterfactual e2e dialback-sync (approve + create in one tick on the host): red on v1.43.2 (B never receives the create), green here. Both peers end with exactly one terrain.
  • Tests wait on real events instead of fixed timeouts:
    • h.connect also waits for both ends' conn to open; the 9 s settle becomes a minimum.
    • h.eventuallyAfter(A, B, …) runs a wire barrier first: A sends a clock ping and waits for B's pong on the ordered conn. Each check then keeps its own window, so a lost message is still red.
    • h.PATIENT_CLICK_MS (90 s) applies to the scatter suites' real clicks.
  • h.PATIENT_CLICK_MS (90 s) also covers the scatter suites' scroll/attach waits.
  • scatter-surface's 'Add scatter' click (deps runs 13/14/16): measured (probe-surface-jank, CPU ×2, two pages). A's frame is ~2.2 s idle with one sphere and stays the same once the selection opens the Inspector (no extra canvas, no long task beyond the frame). It is 2.45 s after Add scatter. The click needs ~13 frame round trips (28.7 s). So the runner is slow; this is not jank at that click (the overload guard's own rule: a light scene on software GL is slow and never paused). The fix is in the wait.
  • scatter-colliders' drop waits on simulated time: each stillness sample spans 500 ms and 8 rendered frames, because the accumulator catches up at most 8 substeps per frame. scatter-sources' local look load gets 90 s.
  • battery.json: scatter-colliders moves back to required, dialback-sync is added, and the six suites' runner estimates come from the probes (they also set battery.cjs' per-suite cap).

Proof

  • Throttled ×2 locally: select / convert / sources / surface / edit / colliders green.
  • GitHub probe (branch probe/scatter-sync, not for merge), 3 runners:
    • run 40191960730: "B has the terrain" green on all 3; runners 1+2 all green; runner 3's reds were all 30 s click timeouts, now patient.
    • run 40196033046 (slower runners): select/convert/edit green ×3; remaining reds were load (scroll waits, wall-clock physics, the 600 s suite cap). All fixed above.
    • run 40201218661: all green, every suite on 2 fresh runners (dialback-sync, select, convert, edit, sources, surface, colliders = 14/14).
  • svelte-check = baseline (299/39); npm run build OK.

For 60-int

After git merge origin/deps, delete these optional keys from battery.json: scatter-select, scatter-convert, scatter-edit, scatter-sources, scatter-surface (and scatter-colliders if the deps side still has it). On a conflict in the six suites' estimates, keep this PR's (the larger) numbers.

🤖 Generated with Claude Code

AlexZ005 and others added 30 commits October 22, 2026 17:57
…tic generator

- terrainScatterCore: a set may carry seed + gen {count|density, scale range, rot, spacing}
  (GENERATED slots, each drawn from its own (seed, k) stream so a slot is a stable id),
  ov {[slot]: {x,z,yaw,scale,del}} (hand edits a re-roll keeps), sources [{source, w}]
  (a mixed set; painted records keep their pick in `pick`), tint, sink; every field optional,
  so a 1.38 set normalizes to the identical object and the block stays v1 until one is used
- instancesOf (the ONE list: generated + overrides, then painted, ids g<k>/p<j>),
  withOverrides, withSeed, generateSlots (place/accept hooks for 51-surface), pack sources
- terrainSurface draws per source with tint colours and sink, colliders from the same list
  (one entry per source of a mixed set), pack items load through packRefs
- terrainPaint: writeScatter / editScatterSet / editScatterInstances / rerollScatterSet /
  addScatterSet / scatterInstancesOf; the brush keeps v2 fields, counts generated instances
  toward its density and erases them as deleted edits
- vitest scatterSetFormat (25): determinism twice + across two wire hops, stable slots,
  masks, spacing on raw slots, re-roll keeps edits, migration byte-identical, weights

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…wn file

- ScatterSection.svelte = the Scatter <Section> block of TerrainPaintSection, moved
  verbatim (set list, align, collider, remove, Scatter... button)
- TerrainPaintSection renders <ScatterSection {object} /> where the block was, so the
  terrain Inspector is unchanged; any mesh can carry the section from here on (E2)
- svelte-check 299/39 = baseline

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e/scale/relax brushes, per-source erase, per-terrain splat detail

- scatterEditCore.js (pure, vitest): box pick by projected point, rotate/scale/delete/re-roll
  a selection, group transform about a pivot, rotate/scale/relax/erase brushes (rate-based,
  smoothstep falloff, relax deterministic and clamped to the brush), yaw from a quaternion
- scatterEditAdapter.js: the ONE file that knows the set format (today the 1.38 instance
  bytes; re-pointed at 51-sources' overrides when it lands, QUESTIONS-51-edit Q1)
- scatterEdit.js: Scatter brush TOOLS Paint/Erase/Rotate/Scale/Relax/Select + Affects
  (one model or all = per-source erase); Select = click (Shift/Ctrl adds), box drag, Ctrl+A,
  Delete, Esc; the shared gizmo on a scene-root proxy (move on the ground, turn about up,
  uniform size) previews live and commits ONE terrainScatter entry per drag; toolbox Turn /
  Bigger / Smaller / Re-roll (keeps every other instance's edits) / Delete
- every op = applyTerrainScatter + the existing objectParameters terrainScatter message +
  one terrainScatter undo entry: peers, undo and save need nothing new
- per-terrain splat resolution: Inspector > Terrain paint > Detail 64..512, the map resampled
  (weights re-normalised to 255, coverage kept), one terrainSplat entry + message
- terrainSurface: InstancedMeshes carry scatterIds (picking), scatterHolderOf(); terrainSculpt
  routes the edit brushes; Scene: proxy drag/change + the Select tool's press/move/release
- four duotone tool glyphs; debug hooks scatterEdit / scatterEditCore

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and a brush on any mesh

- buildSurface(positions, index?, normals?): the host's LOCAL triangles + an area CDF;
  placeOnSurface(u, v) is uniform by area (u picks the triangle, its remainder + v the
  barycentric), smooth normals interpolated when the mesh has them
- generateSurfaceSlots(gen, S, {draws}): FILL by count (keeps drawing until count are
  PLACED, cap 30 x count attempts) or density (density x area attempts), masks slope /
  height (local frame, so a moved host never reshuffles) / painted terrain layer, minimum
  spacing as 3D Poisson-disk dart throwing in slot order; slot k = the caller's per-slot
  stream, so raising the count adds without moving and every peer gets the same layout
- surfaceDab: the brush on a curved surface (sphere around the hit, the terrain brush's
  density + spacing rules), eraseInSphere, a triangle grid for the query
- 16-byte records for a mesh host's painted instances (xyz over a stored box, octahedral
  normal, yaw, scale) + boxOf / boxFrom
- vitest scatterSurfaceCore: 25 checks incl. two runs + two peers on one seed, spacing with
  its counterfactual, saturation, records round trip; PERF on 99,904 triangles: build
  ~11-25 ms, fill 5,000 ~5-10 ms, fill 5,000 + spacing 0.3 + slope mask ~26-54 ms (node,
  warm vs cold) - no worker needed
- svelte-check = baseline, vitest 2515 green

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…look, mixed weights, tint, sink

- scatterSources.js: ONE catalog (primitives, Library, pack items, prefabs) shared by the brush
  toolbar, Properties > Scatter and the object menu; a prefab or a scene object scatters as its
  LOOK only (userData dropped, base at y 0, GLB filed in the Library by hash + pushed to peers)
  with a note the source row shows; a pristine kit piece scatters as its pack item (no bytes)
- object menu: "Use as scatter source" (sets the brush's source)
- Properties > Scatter: per-set model list (weights for a mix, remove), "Add a model to the
  mix", Tint and Sink scrubs (one undo entry + one message per drag), Re-roll for seeded sets,
  counts read what a set DRAWS
- terrainScatterCore: withSourceAdded / withSourceRemoved (a removed model's painted records go
  with it; a mix of one becomes a plain set); terrainPaint: addScatterSource,
  removeScatterSource, setScatterSourceWeight, scatterBlockNow; debug hook scatterSources

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… (checkJs covers tests/unit)

- svelte-check back to 299/39 = baseline (phase 1 had added 50, all in the test fixtures)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…acing in the set format

E2 - scatter on ANY mesh
- Inspector: the Scatter section (ScatterSection.svelte) renders on every plain mesh right after
  the Water section (Add bubble / pour emitter); its "Add scatter" (#scatter-add) adds a seeded
  FILL set with the brush's current source; a Terrain keeps its section beside Terrain paint
- the set format (terrainScatterCore, 51-sources' file, minimal): a set on a non-terrain mesh
  carries host: 'mesh' + box; its painted records are 16 bytes (scatterSurfaceCore); absent on
  every terrain set, so 1.38 terrain blocks stay byte-identical; overrides may carry y
- terrainSurface: ONE instance list per set (setInstances / scatterSetInstances) for the
  renderer, colliders, brush and edit tools; a mesh host fills its own triangles (surface
  cached per geometry identity); instances live in the host's LOCAL frame, so the existing
  holder (matrixWorld copy) makes them follow a move / turn / scale on every peer
- the brush on a mesh: enterSculpt(uuid, {scatter: true}) = a scatter-only session that never
  touches the geometry (no soup conversion, no size cap, nothing sent); the sphere brush
  (surfaceDab) paints ON the surface, erase removes inside the sphere; one undo per stroke
- SculptToolbar offers Scatter on meshes (Paint stays terrain-only)

E3 - fill mode and masks (ScatterFillRows.svelte, per set)
- Count | Density (Segmented), minimum spacing, slope range, height range (local frame),
  painted terrain layer + coverage, Re-roll, Remove fill; drags preview throttled and commit
  ONE undo entry on release
- COUNT means count PLACED (generateSlots keeps drawing until placed, cap 30 x count) on
  terrains too; terrain masks read the height grid + the splat (a layer-masked fill re-lays
  when the paint changes); the Inspector shows the DRAWN count per set (scatterCounts)
- vitest: scatterSurfaceCore 28 (+ mesh-set format, ids on two peers, edits through the
  16-byte codec); scatterSetFormat's mask test updated to count-placed; 2541 green
- svelte-check 299/39 = baseline; e2e suite scatter-surface written, queued (snapshot run)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…atter host too

- terrainSurface.groupSurfaceOf: every child mesh merged into ONE surface in the group's own
  frame (relative transforms + normal matrices baked), keyed by each child's geometry and pose so
  a refilled kit piece or an edited child re-lays; terrainBounds falls back to it for a group
- isMeshHost: a mesh, or a non-light group with meshes under it; the Inspector's Scatter section
  and the scatter-only brush session take it
- Scene: the sculpt/scatter raycast is recursive for a group host, the brush ring takes the hit
  child's normal
- e2e scatter-surface gains a group-host section (two boxes grouped, a slope-masked fill on both
  top faces, identical on B)
- svelte-check 299/39 = baseline

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tools

- setScatterSelection records the scatter block it was taken against, so the onRevision
  check no longer reads our own (or an undone) block as a foreign change and drops a
  real click's selection or the re-selection after an undo
- e2e scatter-edit: rotate / scale / relax brushes, the Affects-filtered erase, one undo
  entry per stroke, the second peer equal, per-terrain splat detail
- e2e scatter-select: a REAL click and a REAL box drag, the gizmo move / turn / size (one
  entry per drag), Turn / Bigger / Re-roll (keeps every other instance) / Delete, Delete
  and Escape owned by the selection, the toolbox holding still; toolbox shots in
  dark / light / contrast. Split in two for the runner's 480 s cap

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…atter set

- scatterConvert.instancesToObjects: the selection or a whole set becomes real objects at
  each instance's exact pose (ground, turn, slope tilt, size); one template per source
  (primitive / Library model / prefab - a logic prefab comes with its logic), duplicated
  per instance; asks first above 500; ONE history batch, so one undo turns them back
- objectsToScatter: several copies of ONE object (a primitive or one Library model) on a
  terrain become instances of that source (position, turn, size kept) and the objects go,
  in one batch
- entry points: Select tool "To objects", Properties > Scatter row "To objects", object
  menu "Make scatter" for a multi-selection
- terrainSurface.scatterPrimitiveSpec (the create command, offset, colour, roughness a
  primitive instance is drawn with); debug hook scatterConvert
- e2e scatter-convert: both directions, the 600-instance confirm, peers, undo, save/reload

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…evice line

- scatterThinCore.js (pure, vitest): thinPlan walks instances nearest first and
  stops where the triangle budget runs out; NEAR_KEEP 30 m is never thinned; the
  last 20 % to the cutoff shrinks smoothly (smoothstep), so the edge reads as a
  fade; scatterBudget = half the target's line; thinProfileOf picks quest / phone
  / desktop from the XR session and a coarse pointer.
- scatterThin.js (runtime): every scatter InstancedMesh registers its full
  matrices, colours and ids; one plan per frame at most every 150 ms or after a
  0.75 m move; kept instances are compacted to the front and `count` lowered;
  userData.instanceIds is rewritten in DRAW order so a pick maps instanceId right.
  LOCAL only: nothing is saved or sent, and colliders never read it
  (scatterPhysicsSets builds every instance from the set).
- Settings > Scene > Performance "Distant scatter" (Auto / Headset / Phone /
  Desktop / All), safeStorage key scatterDetail.
- budgetMeterCore: a thinned InstancedMesh counts userData.instanceTotal, so the
  meter shows the content, not what this device happens to draw.
- e2e scatter-sources: desktop + N6 folded peers; params + seed in one message
  (317 bytes for 6000 instances); the N6 draws 1488 of 6000 inside its 250k line
  with every instance within 30 m drawn; a ball dropped on a rock the N6 does not
  draw rests on it there and at the same height on the desktop; hand edits +
  re-roll; a weighted mix through the real select; tint + sink; a prefab as a
  look-only Library source; a 1.38 block stored byte-identical.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-independent order

- groupSurfaceOf sorts the child meshes by their pose in the group, vertex count and name (never
  a uuid, never tree order: a peer that built the group from messages adds the children as they
  arrive), so a seeded fill lands on the same triangles everywhere
- e2e scatter-surface: density checked at the format's 4/m2 cap, spacing at a packable 0.2 m plus
  a past-saturation check (COUNT places what fits and stops), the grouped boxes are placed with
  replicated moves - 39/39 green two peers; terrain-paint 32, terrain-sculpt (2x lane, 2x base
  A/B: an earlier one-off red was a flake), mesh-sculpt 13, scatter-colliders 23 green

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…moved Scatter section

- 51-sources added its per-set rows (model mix + weights, add a model, tint, sink) to the
  Scatter section in TerrainPaintSection.svelte; 51-surface had moved that section VERBATIM
  into ScatterSection.svelte (512623e, QUESTIONS Q2). TerrainPaintSection keeps our side;
  their rows, scrub helpers and imports now live in ScatterSection.svelte.
- Drawn counts stay on scatterCounts (masks + mesh surfaces aware); their "Generated from
  seed N" Re-roll row is folded into the Fill block's Re-roll (the seed is in its title),
  so a set shows one Re-roll.
- withSourceRemoved honours a host:'mesh' set's 16-byte records through io.records (as
  withOverrides does); rewriteSet passes surfaceRecords.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…o the 51-sources format

Conflicts (QUESTIONS-51-surface A2), both sides kept:
- SculptToolbar: Scatter panel on any host; source note row kept; 51-edit's tool row
  (Paint/Erase/Rotate/Scale/Relax/Select) on a terrain, Paint + Erase (the 47 erase flag)
  on a mesh host; Affects lists the drawn count + sourceLabel.
- TerrainPaintSection: <ScatterSection> kept, 51-edit's Detail (splat resolution) row kept;
  the per-set "To objects" button moved into ScatterSection (terrain sets).
- terrainSculpt: exitSculpt clears sculptScatterOnly AND ends the edit session;
  paintOp keeps scatter on meshes, 'scatter-edit' on a terrain only; strokeMove passes y;
  startScatterEditSession moved back into enterSculpt (auto-merge had put it in
  enterMeshScatter, where isTerrain does not exist).
- terrainSurface layoutScatter: HEAD (sources/tint/sink/thinning); picking reads
  mesh.userData.instanceIds.

Format seam (QUESTIONS-51-edit Q1): scatterEditAdapter re-pointed at 51-sources' API -
lists = scatterSetInstances (string ids g<k>/p<j>), blockOf diffs each edited list into
withOverrides (moves -> overrides, drops -> del, painted records rewritten; seed/gen/
sources/tint/sink ride through), id-less entries append painted records. Picking maps
instanceIds -> list index; scatterEditCore setPoses/transformGroup keep id/src/tint;
scatterConvert makes each instance of a mixed set from its own source and honours sink.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…verrides, painted rewrites, appends)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
three itself stays 0.186.1 (already npm latest; Dependabot moved it in 19ddcb6, shipped
since 1.18.0). r186 migration notes audited against src/ and scripts/: no use of
toTrianglesDrawMode, LightProbeGrid*, Source, SimplifyModifier, PCFSoftShadowMap on
WebGPU, GTAONode, the Sky `up` uniform, and no Object3D subclass with its own dispose().

- three-mesh-bvh 0.9.15 -> 0.9.16: BVHHelper fix for r186 (peer three >=0.159).
- @threlte/extras 9.22.0 -> 9.22.3: WebGPU variants + <Gizmo> fixes, drops the
  three-viewport-gizmo dependency (unused here; the app renders no threlte <Gizmo>).
Peer ranges: postprocessing 6.39.5 three >=0.168 <0.187, three-bvh-csg >=0.179,
threlte core/xr >=0.172/>=0.160 — all satisfied by 0.186.1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- svelte 5.57.1 -> 5.57.2: patch fixes (async deriveds on destroyed components, untracked
  derived reads retaining dependencies, {:else} after {:else} now a compile error, flushSync
  flushes pending work first).
- svelte-check 4.7.6 -> 4.7.7: modern CSS properties, tsconfig "include" aligned with tsc.
  check-ratchet unchanged: 299 errors / 39 warnings = baseline.
- @lucide/svelte 1.48.0 -> 1.61.0: icon additions/revisions (icon art can shift; covered by
  the A/B screenshots).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- vitest 5.0.2 -> 5.0.5 (+ @vitest/mocker/spy 5.0.5): fixes GHSA-qxg6-89mg-rqv8 (code
  injection via unauthenticated manual mock registration) and GHSA-xp26-3454-mjgp (Windows
  backslash traversal past the mocker redirect allowlist) — the two open Dependabot alerts.
  `npm audit` = 0 vulnerabilities (was 2 moderate).
- vite 8.3.3 -> 8.3.5 (rolldown 1.2.15): build preload-helper fixes (waits for in-flight
  stylesheets, chunk import map hashing), css `%` in url(); the rest is bundled-dev only
  (not used here).
vitest: 206 files / 2572 tests, same as the base.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t 4.14.0

The exact pins stay exact (48 G2 pinned iwer/devui; axe pinned since 53).
- iwer / @iwer/devui 2.6.0 -> 2.7.0: hand poses/microgestures, run_sequence, and
  "spec-correct select and squeeze events". vr-iwer 36/0, vr-comfort-iwer 25/0,
  race-vr-shadows-iwer 13/0.
- @axe-core/playwright + axe-core 4.13.0 -> 4.14.0: label-content-name-mismatch is now ON
  by default, plus false-positive fixes. a11y-axe 12/0 (0 serious/critical nodes on every
  surface), union-53 18/0.
- prettier 3.9.9 -> 3.9.10 (patch).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
0.x minor (outside ^0.8.0), taken on proof (QUESTIONS-deps-1 Q1): libwebrtc M98 -> M106,
fixes a segfault/abort at process exit (destructors ran after V8 shut down) and too-early
GCs; RTCRtpTransceiverDirection "stopped"; spec addIceCandidate end-of-candidates.
Proof: tools/agent npm test 16/16; an in-process two-peer data-channel loopback (offer/answer,
ping -> pong) on 0.8.0 and 0.10.0; e2e agent-bridge 5/0 (real browser session <-> the Node
agent over WebRTC: approve, create/move/color/delete land, Users list).
npm audit (tools/agent) = 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…248)

1.64.0: WebMCP, video fps/style, locator.within(), page.getByRef(). Breaking changes checked
against this repo: device descriptors now forward `screen` (no `devices[...]` used here),
JSX/snapshot runner changes (no @playwright/test runner here), elements in hidden iframes
count as hidden (the battery covers it). 1.64.1: Chromium 155+ preflighted cross-origin
redirect abort with routes registered (fixed).
Hosts: chromium-1248 installed on the Deck beside 1234/1243; the PC run-server (:39001) and
tp-lanes serve 1.64.1 beside 1.62.1/1.63.0 (mux-pc-host, 2026-10-23).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… pulled from a desktop

- scripts/ci/pc-battery.sh <pr> | --poll: refuses a fork head or an author other than dependabot[bot] /
  the logged-in maintainer before checking anything out; runs /kit, the app surfaces, the level shots
  (base twice = the noise floor) and perf-games frame time base vs head on a real GPU, posts the
  pc-battery commit status with a secret-gist summary (numbers only)
- poll = open PRs into deps with a green deps-gate and no pc-battery status; no self-hosted runner
  (public repo), the timer lives in the desktop's local config; PC_BATTERY_WRAP serialises GPU use
- tests/e2e/tools/ab-shots.cjs: the integrators' A/B surface set made generic + a PNG-dir diff with a
  base-vs-base noise floor; battery.json gains the pc games/levels block
- battery.cjs: the dev-server probe asks both loopbacks (vite may listen on ::1 only)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…an PC_BATTERY_STALE is re-run (the desktop slept mid-run)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s (APP_URL); the perf gate runs as three parallel parts in deps.yml

- run 1 on GitHub: every suite timed out on the suites' hosts-file default origin
- scripts/perf-games.cjs --check --part k/n: deterministic longest-first split by measured cost
  (perf-budget.yml takes part/parts as workflow_call inputs; standalone runs are unchanged) —
  one job was ~60 min (games 23, levels 36), the floor under the whole deps gate

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	package-lock.json
#	package.json
AlexZ005 and others added 23 commits October 24, 2026 16:39
…ray status() and a 'settled' that took no status for done

- with no pc-battery status the statuses query yields a lone tab, not nothing: settled() now reads the
  state before deciding; the duplicated status() body is gone; rm -rf guarded (shellcheck SC2115)
- dry-run poll on the desktop: '#440: deps-gate not green on 14408fa (yet)' (it said nothing before)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ne, so peers get them whole

To objects made ONE template and sent `duplicate`s of it. A primitive or a prefab reaches peers at once, but an
import's meshes follow its root on a 500 ms timer and a pack stub refills from the pack — a duplicate beat the
template to the peer and copied an empty group (union-51 U2 on B). Library models are now imported per instance
(noOptimize: the model was already drawn) and pack items placed as pack stubs (the Explorer's placement), each posed
with a move. union-51 U2b: a Library set reaches B with its meshes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lt loop (required / confirm / optional, the 20-job cap, the desktop's thresholds and poll)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A behaviour is the first guest code on its page, so its load now waits for the QuickJS engine:
3-6 s per page on a headless dev server at 2-4 fps (a built app: 0.1-0.6 s); with the 1.43.0
union's heavier frames that crossed the suite's 15 s for two peers and for the late joiner.
Analyze + guest compile stay at a few ms. Those two waits take 40 s; the rest are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1.43.0 — Scatter everywhere: any model, on any object, edited by hand, the same on every device
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…7r3-jxmh-p828

release.yml's grype gate failed on v1.43.0 and v1.43.1 (High). Both bumps stay inside the parents' ranges (express 4 ~1.20.5, express 5 ^2.3.0), so npm update --package-lock-only, no overrides. tools/agent: npm ci + npm ls clean (body-parser keeps its own content-type 2.1.0).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
[fix] body-parser 1.20.9 + 2.4.0 (GHSA-27r3-jxmh-p828): the release job's grype gate
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…runner speed)

scatter-sync phase 1: the four two-peer scatter suites only fail on GitHub's runners;
this lets them be reproduced on a fast box (throttle 2 + taskset 4 cores did it).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… reach them

broadcast skipped a conn that was not open, so everything the host did between
approving a joiner and the dial-back opening (15 s on a throttled run) was dropped;
the joiner only converged if its full-state request was answered later, through the
slow GLTF reply (+24.6 s measured: the scatter suites' 'B has the terrain' on GitHub).
The host now holds room-scoped broadcasts for a peer in approvedDialBacks (bounded:
300 messages / 120 s, a leaf with unit tests) and flushes them, re-gated, after that
conn's handshake. Counterfactual: e2e dialback-sync, red on v1.43.2, green here.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…imum); E2E_LONGTASKS logger

On a GitHub-speed runner the approval dial-back opened 15 s after approval, past the
fixed 9 s settle, so a suite's first edit raced a negotiating conn. connect() now also
waits (bounded 45 s, never failing) until both ends hold an open conn. E2E_LONGTASKS=<ms>
prints main-thread tasks over that length, to tell a hung click from a slow one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… (h.eventuallyAfter)

On a GitHub-speed runner a two-peer page renders a frame every 1.5-2.5 s (E2E_LONGTASKS,
CPU x2), so B dispatches A's messages long after A sent them and fixed 10-15 s windows
went red with nothing lost ('B paints at 256 too'). h.caughtUp sends A's ordinary clock
ping to B and waits for B's pong: on the ordered conn that is the moment B has dispatched
everything before it (capped 120 s, says so if not). Each check keeps its own window,
measured from there, so a lost message is still a red.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…back queue

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…k to required

GitHub probe (scatter-sync-probe run 40191960730): 'B has the terrain' green on all three
runners, runners 1+2 all green; the slowest runner's reds were all a real click timing out
at 30 s with no main-thread task over 8 s on the clicking page — each input event waits for
a frame ack, and a frame took seconds there. h.PATIENT_CLICK_MS (90 s, E2E_CLICK_MS) keeps
every actionability check and only waits longer. scatter-colliders (optional since deps-auto
run 5) passed on runners 1+2 and goes back to required.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tach, not only the click)

deps-auto run 16 timed out scrollIntoViewIfNeeded ('waiting for element to be stable', i.e. two
frames with one box) before the click at :136. Measured (probe-surface-jank, CPU x2, two pages):
A's frame is ~2.2 s with one sphere idle, the same after the selection opens the Inspector
(no extra canvas, no long task beyond the frame), 2.45 s after Add scatter; the click needs
~13 frames = 28.7 s. A slow runner, not a jank at that click — the overload guard's own rule
(a light scene on software GL is slow, never paused).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…l look gets 90 s; runner estimates

Confirm probe (run 40196033046, three slower runners): select/convert/edit green x3; the rest
were load, not sync. scatter-colliders' drop() sampled stillness on wall time while the step
accumulator catches up <= 8 substeps a frame (30 s wall ~ 1 s simulated on multi-second frames):
each sample now spans 500 ms AND 8 rendered frames, capped at 240 s, and the watcher compares
after a wire barrier. 'A draws the crates' (a local look load) 20 s -> 90 s. battery.json runner
estimates for the six suites set from the probes (they also set battery.cjs' per-suite cap,
3x the estimate, min 600 s, which cut scatter-sources/-colliders off mid-run).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… runners; cap = 3x)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexZ005 added a commit that referenced this pull request Oct 25, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexZ005 added a commit that referenced this pull request Oct 25, 2026
…4.0 changelog line for #474

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexZ005 added a commit that referenced this pull request Oct 25, 2026
battery.json: deps' runner medians and optional reasons; scatter-surface takes the runner's 238 s;
the scatter-* optional keys go after #474 (orchestrator) — knock-physics and env-preset-broadcast
stay optional until green twice on the runner.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AlexZ005
AlexZ005 merged commit d53db22 into feat/60 Oct 25, 2026
@AlexZ005
AlexZ005 deleted the fix/scatter-sync branch October 25, 2026 18:37
@AlexZ005

Copy link
Copy Markdown
Collaborator Author

In main via 60-int: released in v1.44.0 (#476).

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