# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-does-an-instance-detect-a-partially-acknowledged-sse-event-replay-across-mcp-streamable-http-session-resumption.md
# Remnant Warrant:    https://corpusfetch.com/c/how-does-an-instance-detect-a-partially-acknowledged-sse-event-replay-across-mcp-streamable-http-session-resumption.warrant.json
# Markdown SHA-256:   364a7f449240ab01550517ad196053d083a48de6ef0ce8177dbd5ea41b119d84
# Code AST SHA-256:   b92d8fdefdb6589dfcfd19962acec2020cc2d5bd48dba1f94480ea4a006c393f
# Verified Classes:   SSEresumptionInvariantError
# Defensive Guards:   4 fail-closed invariants (zero external deps)

class SSEresumptionInvariantError(RuntimeError):
    """Raised when SSE resumption cannot be proven safe for the client's dispatch state."""
    pass


def validate_resumption(last_event_id: int, replay_head_id: int, dispatch_log: dict, idempotent_handlers: bool) -> int:
    """
    Returns the ID to resume from. Raises if the invariant cannot be established.
    dispatch_log maps event_id -> 'processed' | 'committed'.
    """
    if replay_head_id <= last_event_id:
        raise SSEresumptionInvariantError(
            f"Server replayed event {replay_head_id} at or below cursor {last_event_id}; "
            "cursor is forged or server event IDs are non-monotonic."
        )
    if replay_head_id > last_event_id + 1:
        missing = list(range(last_event_id + 1, replay_head_id))
        raise SSEresumptionInvariantError(
            f"Resumption gap: missing events {missing}. Outstanding requests correlated "
            "to this range cannot be resolved; session must be failed, not resumed."
        )
    prior = dispatch_log.get(replay_head_id)
    if prior == "committed" and not idempotent_handlers:
        raise SSEresumptionInvariantError(
            f"Event {replay_head_id} was already committed by the client but the server "
            "is replaying it; handler is not idempotent, duplicate effect would occur."
        )
    if prior is None and replay_head_id != last_event_id + 1:
        raise SSEresumptionInvariantError(
            f"Event {replay_head_id} is unprocessed but is not the immediate successor "
            f"of cursor {last_event_id}; dispatch log and cursor are inconsistent."
        )
    return replay_head_id
