282a0e70ad
EnsureDistributorCreated read a null _eventDistributor as "not created yet", which is true before MarkReady and false after DisposeAsync nulled it. An attach that passed its readiness gate while the session was still Ready and then lost the CPU across the whole close + dispose therefore built a REPLACEMENT distributor: startNow came back false because the pump-start flag is a one-way latch, so the subscriber registered on a distributor whose pump never starts and whose channel is never completed, and EventStreamService blocked in WaitToReadAsync until the client's own deadline — no error, no log, no metric, the backlog gauge reporting 0 forever. This is the disposal-side twin of the premature-attach poison GWC-27 closed. DisposeAsync now sets _eventDistributorDisposed under _syncRoot in the same section that nulls the distributor, and EnsureDistributorCreated throws SessionManagerException(SessionNotReady) when it is set. The attach paths' existing catch/rollback releases the subscriber slot, so a second attempt still reports SessionNotReady rather than EventSubscriberAlreadyActive. StartDashboardMirror is the one caller that is not an attach: it abandons the mirror (releasing its loop token source) instead of propagating, so MarkReady stays total. Losing that race previously left a started orphan distributor behind, so this path strictly improves too. Test: AttachAfterDisposeThrowsSessionNotReadyInsteadOfRegisteringOnANeverPumped- Distributor covers all three attach entry points plus the rollback, and fails on the pre-fix code. docs/Sessions.md records the disposal end of the one-distributor-per-session rule.