---
phase: 06.4
plan: 09
status: complete
verdict: in-scope-fix
completed: 2026-05-16
requirements: [REQ-SRV-03, REQ-CLI-08, REQ-CLI-09]
commits:
  - b312f63
  - 7deb4c5
  - 54a328d
  - aa1f54e
  - a426d2c
---

# Phase 06.4 Plan 09: D-51c Reconnect / Eviction Loop Summary

[doc->REQ-SRV-03] [doc->REQ-CLI-08]

## Verdict

**Complete: in-scope project-owned fix.** D-51c did not require a Colyseus 0.17 to 0.18 upgrade. Staging telemetry showed two project-owned lifecycle bugs, both fixed in 06.4.

## Spike Classification

The issue classified as option-a: in-scope fix. The staged traces pointed to client/room lifecycle handling, not a missing Colyseus API.

Evidence:

- 2026-05-16T08:42Z reconnectLog: `client.reconnect(token)` failed with `room "<id>" has been disposed`, then fresh join succeeded and received `force_reset`.
- 2026-05-16T08:54Z reconnectLog: no cached token, fresh join succeeded mid-session, then received `force_reset` reason `replaced_by_new_session`.
- 2026-05-16T09:14Z reconnectLog after final fix: fresh join succeeded and cached a token; no `force_reset`, no `client_onLeave`, no reconnect loop.

## Root Causes

1. **Consent teardown on generic scene disposal.**
   `GameScene.dispose()` called `room.leave()` during generic Phaser scene teardown. That sends a consented Colyseus leave, bypassing `allowReconnection()`, deleting the player state, and allowing the MVP room to dispose before `client.reconnect(token)` can resume it.

2. **Duplicate connect while the room was still healthy.**
   After removing generic `room.leave()`, the remaining trace showed a no-token fresh join while the old room was still alive. That opened a second room for the same account in the same tab, so the server correctly emitted `replaced_by_new_session` against the older connection.

## Fixes

- `GameScene.dispose()` no longer calls `room.leave()` for generic teardown. Explicit logout and `force_reset` remain the consented leave paths.
- `RebnoRoom` sets `autoDispose = false` because the MVP room is a persistent world rather than an ephemeral match.
- `GameScene.connect()` suppresses duplicate joins while `this.room` exists and has not reported `onDrop`, `onLeave`, or `onForceReset`.
- `window.__rebno.reconnectLog` records reconnect attempts, reconnect failures, fresh-join fallthrough reasons, and token cache/clear reasons.
- `window.__rebno.sceneLifecycleLog` records create/connect/shutdown/dispose and duplicate-connect suppression events.
- Server disconnect cleanup zeroes held velocity/axis fields before state delete/reap so observers do not see a stale remote run forever.
- `PlayerRenderer` locally zeros stale remote held velocity after snapshots stop.

## Chosen Fix Path

The plan's candidate paths were server seat tweak, eviction loop guard, or state cleanup ordering. The final fix path was **client lifecycle guard + room persistence**, with server cleanup hardening:

- `apps/client/src/scenes/GameScene.ts`: generic dispose no longer performs consented leave; duplicate connect guard added; scene lifecycle telemetry added.
- `apps/server/src/RebnoRoom.ts`: MVP room persistence via `autoDispose = false`; cleanup zeroes state velocity/axis fields before deletion.
- `apps/client/src/net/colyseus-client.ts`: token lifecycle telemetry and reconnect/fresh-join classification.
- `apps/client/src/render/PlayerRenderer.ts`: stale remote velocity guard.

This is intentionally not an eviction-loop retry guard. The server's duplicate-session policy remains valid; the bug was the same tab creating conditions that looked like a legitimate duplicate session.

## Verification

- PASS: `pnpm --filter @rebno/client typecheck`
- PASS: `pnpm --filter @rebno/server typecheck`
- PASS: `pnpm --filter @rebno/client exec vitest run src/__test__/game-scene.test.ts src/__test__/colyseus-client.test.ts src/__test__/reconnect.test.ts`
- PASS: `pnpm --filter @rebno/client test`
- PASS: `pnpm --filter @rebno/server test`
- PASS: staging deploy run `25958052861`
- PASS: operator UAT 2026-05-16T09:14Z: connection stable; reconnectLog contains only fresh join + token cache + success.

## Operator Result

Operator reported: "connection seems stable now." The provided reconnectLog contains:

- `fresh_join_fallthrough` reason `no_cached_reconnection_token`
- `fresh_join_attempt`
- `reconnect_token_cached` reason `fresh_join_success`
- `fresh_join_success`

No `force_reset_received`, `reconnect_failure`, `client_onLeave`, or duplicate fresh join is present.

## Follow-Up

No Phase 06.5 Colyseus-upgrade phase is needed for this bug. The remaining 06.4 closure item is the formal CLI-08 mp4 artifact from plan 16.
