# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-the-sse-resumption-cursor-has-silently-desynchronized-from-the-mcp-session-s-stream-epoch.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-the-sse-resumption-cursor-has-silently-desynchronized-from-the-mcp-session-s-stream-epoch.warrant.json
# Markdown SHA-256:   a247fb129d0a9f9ffd9ca8c37f7271d66e3f17cd438ce3fb114f3f9a42decca0
# Code AST SHA-256:   2af8a02f3a8a967cfa0d86a9af4ef3662dc6fdf0dfe3704d37d070330242ee09
# Verified Classes:   StreamEpochDesyncError
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class StreamEpochDesyncError(RuntimeError):
    """Raised when a resumed SSE cursor violates the session's epoch/monotonicity contract."""
    pass


def validate_resumed_cursor(last_seen_id: int, first_replayed_id: int, same_epoch: bool) -> None:
    """
    Called on the first SSE frame after a reconnect that presented Last-Event-ID.
    Enforces: same epoch, and first replayed id == last_seen_id + 1.
    Anything else is a desync and must NOT be resumed.
    """
    if not same_epoch:
        raise StreamEpochDesyncError(
            f"resumed into a different stream epoch: last_seen={last_seen_id}, "
            f"first_replayed={first_replayed_id}; buffer was reset or session recycled"
        )
    if first_replayed_id <= last_seen_id:
        raise StreamEpochDesyncError(
            f"cursor regression: last_seen={last_seen_id}, first_replayed={first_replayed_id}; "
            f"server buffer wrapped or counter restarted"
        )
    if first_replayed_id != last_seen_id + 1:
        raise StreamEpochDesyncError(
            f"event gap across reconnect: expected {last_seen_id + 1}, "
            f"got {first_replayed_id}; events {last_seen_id + 1}..{first_replayed_id - 1} lost"
        )
    # Contract satisfied: safe to continue consuming this stream.
