b70c04b8

Author: Michael Lynch <git@mtlynch.io>

Fix session takeover signaling and slim the reconnect plumbing

The replaced-tab flow never worked over the wire: stopping a channel
with a non-shutdown reason reaches phoenix.js as a bare "phx_error"
with an empty payload, so the client classified takeovers as
"reconnecting" and auto-rejoined, letting two tabs on one session
steal it from each other forever. The server now pushes an explicit
"session_replaced" event before stopping with {:shutdown, :replaced},
and the client reacts to that event alone. Covered by a channel test,
a network unit test, and a two-tab e2e test.

Robustness and performance:
- Suspended sessions are pruned after a retention period
  (session_retention_ms, default one hour); previously state.sessions
  grew by one entry per page load forever.
- A connections index maps connection_ref -> session_id, replacing the
  per-message linear scan over all sessions (including 20 Hz
  report_state casts and per-tick delivery).
- Session tokens now expire after 30 days instead of never.
- Token verification moved from socket connect to channel join so a
  stale token (rotated secret, expired) surfaces as a join error the
  client can see; app.ts then discards the stored token and reloads
  once instead of wedging in a silent reconnect loop.

Simplifications:
- Removed world_instance_id/revision from every payload and event;
  nothing consumed them. Removed the unused onRejoin/onStateChange
  handler hooks.
- Inventory counts now flow only through inventory_changed totals;
  pickup events carry ids for sprite removal but no counts, and the
  event is only sent when totals actually changed.
- World events deliver synchronously instead of bouncing through a
  send-to-self hop, restoring event-before-snapshot ordering.
- One fenced-push helper in Network drops stale replies for all call
  sites, replacing four hand-rolled connectionCycle guards.
- disconnect_grace_ms has one default (State.disconnect_grace_ms/0);
  the config entry and struct default duplicating it are gone.
- Texture creators are idempotent, so scene restarts call them
  unconditionally instead of guarding at every call site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
check-bashcheck-elixircheck-frontendcheck-trailing-newlinecheck-trailing-whitespacedefaulte2e-testsconfiguredevShellx86_64-linuxcheck-bashcheck-elixircheck-frontendcheck-trailing-newlinecheck-trailing-whitespacedefaulte2e-testsmikevillex86_64-linux