# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-distinguish-replayable-sse-stream-state-from-non-replayable-websocket-frame-state-during-mcp-session-migration.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-distinguish-replayable-sse-stream-state-from-non-replayable-websocket-frame-state-during-mcp-session-migration.warrant.json
# Markdown SHA-256:   e7a864a3d2077378330679b535839e6b6f5d8879231b96e6340747e1e7449ea8
# Code AST SHA-256:   c6fcffe6a669165e28ecc83998f9c1e57d823f5b890d9d05aa37c2a1c5d2901b
# Verified Classes:   SessionMigrationGapError, McpSession
# Defensive Guards:   2 fail-closed invariants (zero external deps)

class SessionMigrationGapError(RuntimeError):
    """Raised when a WebSocket migration would skip unreplayed SSE events."""
    pass


class McpSession:
    def __init__(self, session_id: str, last_ack_id: int, replay_buffer: dict[int, dict]):
        self.session_id = session_id
        self.last_ack_id = last_ack_id
        self.replay_buffer = replay_buffer  # event_id -> payload, bounded
        self.last_emitted_id = max(replay_buffer) if replay_buffer else 0

    def migrate_to_websocket(self, client_cursor: int) -> list[dict]:
        if client_cursor != self.last_ack_id:
            raise SessionMigrationGapError(
                f"client cursor {client_cursor} != server ack cursor "
                f"{self.last_ack_id} for session {self.session_id}; "
                f"client may be echoing a server-emitted ID"
            )
        gap = [
            self.replay_buffer[eid]
            for eid in range(client_cursor + 1, self.last_emitted_id + 1)
            if eid in self.replay_buffer
        ]
        expected = self.last_emitted_id - client_cursor
        if len(gap) != expected:
            raise SessionMigrationGapError(
                f"replay buffer cannot cover gap for session {self.session_id}: "
                f"need {expected} events after id {client_cursor}, "
                f"have {len(gap)}; buffer evicted {expected - len(gap)}. "
                f"Refusing upgrade; client must resume SSE."
            )
        # Safe to upgrade: gap frames are replayed first, then live frames follow.
        return gap
