Skip to content

Fix pre-release defects from the v1.0.30 regression audit - #747

Open
brentyi wants to merge 7 commits into
mainfrom
claude/regressions-v1-0-30-5wehcp
Open

Fix pre-release defects from the v1.0.30 regression audit#747
brentyi wants to merge 7 commits into
mainfrom
claude/regressions-v1-0-30-5wehcp

Conversation

@brentyi

@brentyi brentyi commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

brentyi added 2 commits July 26, 2026 15:19
Python:
- Initialize _share_tunnel before the atexit registration so a failed
  __init__ can't leave the atexit stop() recursing through the
  backwards-compat __getattr__ shim at interpreter exit.
- request_share_url now closes and replaces a dead ("failed"/"closed")
  tunnel instead of returning None forever; ViserTunnel.close()
  tolerates the failed connection's already-closed loop.
- GuiTabGroupHandle.remove() tombstones under _panel_lifecycle_lock
  (mirroring PanelHandle.remove) so a racing add_tab can't register an
  orphan container for a dead group.
- min/max_orbit_distance setters validate finiteness, positivity, and
  min<=max before the no-op short-circuit.
- _colors_to_int_tuple materializes its argument once so generator
  inputs can't be half-consumed by the warning pass.
- Async callback dispatch reachable from user threads (on_client_connect
  replay, value-setter update callbacks, form submit, pointer-done
  cleanups) now uses run_coroutine_threadsafe instead of the
  non-thread-safe loop.create_task.

Client:
- App.tsx reads the mobile media query synchronously on first render so
  mobile-width loads no longer mount the canvas inside the dock surface
  and immediately remount it (throwaway WebGL context + GPU re-upload).
- The mobile bottom-sheet panel list subscribes with value-level
  equality, so visible/order/tab-metadata updates propagate.
- InstancedMesh2.dispose() nulls instanceIndex and drops the geometry
  attribute so a frame drawn in the dispose->swap window re-initializes
  instead of binding a deleted GL buffer.
- The splat sort worker tracks buffer dirtiness so an updateBuffer that
  lands during a running sort is re-sorted even with a stationary
  camera.

Dock (spec-traced per dock-ux-spec.md):
- All resize surfaces (region resizer, docked dividers, stack divider,
  window grips) share the one-gesture slot move-drags already used, so
  a second pointer can't start a concurrent gesture (spec 4).
- Hit-testing: vertical tab insertion gets the same proximity bound as
  the horizontal bar path so rail merge is reachable (spec 5.3); the
  region side band yields across its overhang of a collapsed cell so
  one seam yields one drop with no hint hop (D55); sub-8px bottom
  split/snap bands are removed rather than shrunk (P11).
- The stack divider's cancel/motionless-press path commits the stored
  pinned height instead of the capped rendered height (P2/D56).
- Escape during a region resize restores float positions pushed by
  pushFloatsAheadOfSeam, not just widths (spec 4, P2).

Every behavioral fix is pinned by a regression test verified to fail on
the pre-fix code: 5 Python (tunnel recovery, orbit validation, color
coercion, atexit exit, tab-group race) and 11 client assertions across
hit-testing, divider cancel, float restore, splat staleness, and
dispose. Gates: ruff check/format clean, 689 Python tests pass; tsc,
eslint, prettier clean, 630 client tests pass.
The new setter validation rejected non-finite values, which removed the
only opt-out from the 1e4 default: infinity is the pre-1.1.0 unbounded
behavior and the camera-controls default, and large-scale scenes need
it. Accept +inf for the max bound (NaN and <= 0 still raise); the min
bound stays strictly finite.
@brentyi
brentyi force-pushed the claude/regressions-v1-0-30-5wehcp branch from 37b58d6 to e922562 Compare July 26, 2026 15:19
brentyi and others added 5 commits July 26, 2026 15:31
A local npm 10 re-resolve during the gate runs dropped the
@emnapi/core and @emnapi/runtime entries that @napi-rs/wasm-runtime
references, which npm 11's stricter 'npm ci' sync check rejects. No
dependency changes were intended; this restores main's lockfile
byte-for-byte.
Two coverage holes that hid this cycle's worst bugs:

- test_mobile_breakpoint.py: a mobile-width load must not mount the
  canvas desktop-first and remount it (counts WebGL context creations
  and canvas removals against a desktop self-baseline), and the mobile
  bottom sheet must reflect server-side panel visibility updates.
- test_touch_gesture_mutex.py: while a divider resize is in progress
  via one touch, a second concurrent touch on a floating window's grip
  must be ignored (spec 4, one active gesture at a time), driven as
  real multi-touch through CDP Input.dispatchTouchEvent.

Both verified to fail against a client built from the pre-fix sources
and pass against the fixed build.
…uite

Twice on CI (py3.9 shard 2: runs 30298026392 attempt 1 and 30418320431)
the per-worker Vite dev server wedged permanently partway through the
run: page.goto kept succeeding but the module graph never completed, so
the dock never mounted and all remaining playground tests on that worker
burned the full 60s timeout twice each -- 17 consecutive failures over
~30 minutes, with orphaned esbuild children left behind (the dependency
optimizer hanging is the prime suspect). The failure onset shifted
between the two runs, so it is not tied to a specific test, and unlike
the documented 2026-07-18 starved-runner incident it never recovers on
its own -- which means the existing generous-timeout hardening cannot
help. Waiting cannot rescue a wedged server; a restart can.

- tests/e2e/vite_manager.py: extract the dev-server process management
  from the conftest fixture into a restartable ViteServerManager. The
  server now runs in its own process group and teardown signals + waits
  on the WHOLE group (escalating to SIGKILL), fixing the esbuild orphan
  leak: `proc.terminate()` only killed the npx wrapper, and waiting on
  the direct child returned as soon as npx died while node/esbuild
  lived on. stdout/stderr now go to a log file instead of DEVNULL, and
  the tail is printed on restart so a future wedge is diagnosable.
- dock_helpers.open_playground: after the existing two page-boot
  attempts fail, restart the worker's Vite server and make one final
  attempt against the fresh process -- turning the 30-minute cascade
  into a ~2 minute self-heal with zero test failures. The third attempt
  only exists when a managed server was actually restarted.
- conftest: the vite_server fixture now just owns a manager's lifetime.

Validated end-to-end outside CI: normal boot, SIGSTOP-simulated wedge
(two failed attempts -> restart with log tail -> healed boot on the
same port), old process group fully reaped, and teardown leaving no
processes. ruff check/format clean; playground tests pass through the
new fixture path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXMGHKyd8kaEDcVZ85qzHf
An advocate/critic subagent panel reviewed the PR's effective diff vs
main. The advocate independently re-ran the falsification protocol (all
new Python and client regression tests fail on pre-fix sources, pass on
the branch); the critics surfaced the defects fixed here, each verified
against the code before fixing.

Tunnel (should-fix): the dead-tunnel replacement path leaked one
SyncManager child process + watcher thread per request_share_url retry
while the share backend was unreachable. A tunnel that ends "failed"
never sets _connect_event, so on_connect's wait_job blocked forever,
pinning the mp.Manager proxies; pre-fix the leak was bounded at one
because the failed tunnel poisoned the slot, but the replacement branch
creates a fresh tunnel per retry. ViserTunnel.close() now releases the
watcher (wait_job gains a status guard so the release cannot fire the
connect callback; _connect_job stores "connected" before setting the
event, making the guard race-free). Pinned by
test_close_of_failed_tunnel_releases_watcher_thread, verified to fail
pre-fix. Also: the failed tunnel's on_disconnect no longer broadcasts
ShareUrlUpdated(None) when a newer tunnel owns the slot -- the message
persists in the broadcast buffer, so the stale None also hit late
joiners.

InstancedMesh2 (should-fix, vendored -- LOCAL_PATCHES.md updated):
a frame drawn in the dispose->unmount window crashed the shadow pass:
shadow maps render BEFORE the main pass, onBeforeShadow only guarded
its culling call, and the unconditional instanceIndex.update()
dereferenced the nulled attribute (any shadow-casting batched mesh,
the Python API default). onBeforeShadow now has the same null gate as
onBeforeRender. LOD levels are separate meshes with per-level
geometries, so dispose() now drops each level geometry's retained
instanceIndex attribute too, instead of leaving them binding the freed
GL buffer. Both pinned by new dispose tests, verified to fail pre-fix.

Mobile sheet test gap (should-fix): panelUpdates.test.ts pinned the
equality utilities but not the component wiring -- reverting
PanelsFallback to key-set equality left the suite green. The comparator
choice now lives in panelsEquality.ts (imported by the component), and
the test pins both the module identity and, via a ?raw source
assertion, the selector call site.

Harness nits: vite_manager falls back to direct-child terminate on
Windows (os.killpg/getpgid are POSIX-only; the old fixture worked
everywhere), and a failed mid-suite vite restart no longer masks the
page-boot diagnostic (open_playground re-raises the page error with the
restart failure chained).

Gates: ruff check/format clean; targeted Python suites 49/49 (new leak
test fails on pre-fix code); tsc, eslint, prettier clean; client suite
637/637 with the four new pins failing on pre-fix sources.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXMGHKyd8kaEDcVZ85qzHf
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