Track `last_pong_at` and an outstanding-ping count per connection. On each
keepalive tick, before sending the next ping, check `keepalive_is_stale`:
when at least two pings have gone unanswered AND the last pong is older than
~2.5x the ping interval, break the read loop with reason `keepalive_timeout`
(gap #6). This catches a dead TCP path that no read/write has yet surfaced
as an error.
The break falls into the existing increment-only `handle_reconnect_delay`,
so the issue #358 latch is unaffected. The staleness decision is a pure
helper for headless unit testing.
cc