# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-happens-when-an-instance-negotiates-an-sse-to-websocket-upgrade-mid-stream-under-mcp-http-native-transport-with-resumable-event-ids.md
# Remnant Warrant:    https://corpusfetch.com/c/what-happens-when-an-instance-negotiates-an-sse-to-websocket-upgrade-mid-stream-under-mcp-http-native-transport-with-resumable-event-ids.warrant.json
# Markdown SHA-256:   9b72158e703aef2d2676c7536650c56717a8d16924caa8b543700943ab11edfb
# Code AST SHA-256:   2c2e10f1a28d7f62bf64a34319efd857da8d9315b5f1ec7493f1559ed3374c48
# Verified Classes:   CursorDiscontinuityError
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class CursorDiscontinuityError(RuntimeError):
    """Raised when an SSE-to-WebSocket upgrade cannot preserve the resumption cursor."""
    pass


def negotiate_upgrade(
    prior_event_id: int,
    committed_cursor: int,
    echo_in_handshake: bool,
    replay_buffer_floor: int,
) -> int:
    """
    Validate that an SSE-to-WebSocket upgrade is resumable.

    Returns the cursor the WebSocket domain must start from.
    Raises CursorDiscontinuityError if the upgrade would create a silent gap.
    """
    if not echo_in_handshake:
        raise CursorDiscontinuityError(
            f"upgrade omits cursor echo; client at eid={prior_event_id} "
            f"cannot reconcile with committed cursor {committed_cursor}"
        )

    if committed_cursor < prior_event_id:
        raise CursorDiscontinuityError(
            f"committed cursor {committed_cursor} regressed behind client "
            f"eid={prior_event_id}; replay log truncated?"
        )

    if replay_buffer_floor > prior_event_id:
        raise CursorDiscontinuityError(
            f"replay buffer floor {replay_buffer_floor} exceeds client "
            f"eid={prior_event_id}; fallback SSE reconnect would lose events"
        )

    # Gap between prior_event_id and committed_cursor is recoverable only
    # because the echoed cursor forces the client to ack before frames flow.
    return committed_cursor
