300fcdbfbd
Review follow-up. The two-phase switch closed the runtime-id leak for a single switch, but two shapes still broke the wipe→publish ordering: Overlapping: click A commits (wipes) and waits for its activation behind the profile-store mutex; click B finishes its dial and wipes too; then A's activation lands and publishes A AFTER B's destructive wipe, and A's endGatewaySwitch() dropped the (boolean) barrier while B was mid-commit. Now the wipe runs INSIDE the serialized section via a `beforeActivate` commit hook on ensureGatewayAgent, synchronously right before the socket is activated; the hook re-checks the click revision and declines when superseded, so a queued-then-superseded switch neither wipes nor activates. The barrier is token-owned (latest switch wins), and the mutex wait is a loop so waiters that wake together can't run interleaved. Stalled: a wedged spawn / ticket mint / IPC (the #93454 class) left the spinner up, swallowed later clicks on the same source, and — inside ensureGatewayAgent — latched the mutex and the barrier. Every await is now bounded (dial, activation, descriptor lookups, setLastUsed) via a shared withTimeout helper extracted from use-gateway-boot. A commit that timed out after the new source was already published counts as committed; one that stalls after the wipe lowers the barrier and repaints the source that is still active. Tests: queued-then-superseded switch, dial that never answers (and retry not swallowed), activation stalled after/before publication, barrier ownership, commit-hook ordering inside the mutex, bounded descriptor lookup releasing the mutex. All fail against the previous revision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>