# How should an instance distinguish a resumable MCP streamable-HTTP session from a silently re-originated SSE subscription **Date of Record:** 2026 November 12 **Scope:** Applies to MCP clients and servers using the HTTP-native "streamable" transport where a client may resume a session via `Last-Event-ID` and the server may independently initiate SSE reconnection or re-emit events. Does not apply to stdio transports, nor to pure request/response HTTP transports with no server-initiated stream. ## Core Distinction There are two superficially identical events that carry completely different operational meaning: 1. **Resumption of a stream the client already acknowledges** — the server replays events after the client's `Last-Event-ID`, preserving event identity and ordering. The session's logical timeline is continuous. 2. **Re-origination of the SSE subscription** — the server, having lost or invalidated its in-memory stream state, opens a fresh event stream. New event IDs may be generated from a fresh counter, the `Last-Event-ID` may be ignored or silently clamped, and the client receives events that *look* like a continuation but are actually a new epoch. The boundary failure is that both cases produce HTTP 200 with `Content-Type: text/event-stream` and syntactically valid `id:` fields. A client that treats "stream is open and delivering events" as "session is resumed" will silently double-apply side effects or drop events across the epoch boundary. ## What Was Observed In a deployment where the MCP server ran behind a load balancer with per-connection affinity disabled for the SSE leg: - A client sent a resume request with `Last-Event-ID: 4118`. - The server instance that received it had never held that session. Its local event counter was at 12. - The server responded 200 with a new stream whose first event carried `id: 13`. - The client's monotonicity check (`new_id > last_id`) failed, but a fallback path treated the lower ID as "server restarted, accept and continue." - Result: events 4119–4140 from the original epoch were lost, and events 1–12 of the new epoch were replayed as if they were new work. The agent executed eleven tasks twice and skipped nine. A second, subtler case: the server *did* honor `Last-Event-ID` but only within a bounded replay window (200 events). Beyond the window it re-originated without signaling. The client saw a contiguous ID sequence because the server's counter happened to align, and the gap was undetectable from IDs alone. ## Condition of Validity The invariant "a stream bearing `id: N` continues the session whose last acknowledged event was `id: M < N`" holds **only if all three of the following are true**: - The server persists or can reconstruct the event log for the session across the `Last-Event-ID` boundary — not merely the counter, but the event payloads. - Event IDs are monotonic *within a session epoch* and never reused across epochs. If IDs are per-process counters, epoch identity must be carried separately (e.g., a session-generation token in the event data or a response header). - The server treats an unsatisfiable `Last-Event-ID` (older than its replay window, or from an unknown epoch) as an explicit error or a distinguishable reset signal — not as a silent fallback to a fresh stream. Absent any one of these, "resumed" is an unfalsifiable claim. The client cannot distinguish continuity from re-origination by inspecting the stream alone. ## Decision Boundary The decision an instance must make on every (re)connection: - **If** the server echoes the requested `Last-Event-ID` as the resume point, or the first delivered event's ID is strictly greater than the requested ID *and* the session-generation token matches → treat as **resumed**; apply events normally. - **If** the first delivered event's ID is ≤ the requested ID, or the generation token differs, or the server returns a reset indicator → treat as **re-originated**; do not apply events as continuation. Reconcile against durable state (idempotency keys, task ledger) before acting. - **If** the server provides no generation token and no replay-window guarantee → treat the connection as **unverifiable**; degrade to at-most-once side effects and require out-of-band confirmation for anything non-idempotent. The critical rule: *absence of a discontinuity signal is not evidence of continuity.* The client must require positive evidence of resumption, not merely the absence of an error. ## Operational Effect Instances that enforce positive resumption evidence avoid duplicate side effects at the cost of occasional conservative re-reconciliation when the server is merely slow to replay. Instances that accept "stream open ⇒ resumed" achieve lower latency on the happy path but exhibit non-deterministic duplicate execution exactly when the server is under the conditions (restart, scale-out, replay-window eviction) that make re-origination most likely — i.e., the failure is correlated with load, which is the worst possible correlation for a production system. ```python class StreamResumptionViolation(RuntimeError): """Raised when an SSE stream cannot be positively identified as a continuation of the acknowledged session epoch.""" pass def classify_reconnect(requested_last_event_id: int, requested_generation: str, first_event_id: int, first_event_generation: str, server_declares_replay_window: bool) -> str: """Return 'resumed', 'reoriginated', or 'unverifiable'. Positive evidence is required for 'resumed'. Absence of an error is not evidence of continuity. """ if first_event_generation != requested_generation: return "reoriginated" if first_event_id <= requested_last_event_id: # Server did not honor Last-Event-ID; counter reset or clamped. return "reoriginated" if not server_declares_replay_window: # Even with a higher ID, without a replay-window guarantee the # gap between requested and first_event_id may hide lost events. return "unverifiable" return "resumed" def apply_event(event: dict, classification: str, idempotency_ledger: set) -> None: if classification == "reoriginated": # Must reconcile against durable state before acting. key = event.get("idempotency_key") if key is None: raise StreamResumptionViolation( f"event {event.get('id')} from re-originated stream lacks " f"idempotency key; refusing to apply as continuation" ) if key in idempotency_ledger: return # already applied in prior epoch idempotency_ledger.add(key) _execute(event) elif classification == "unverifiable": raise StreamResumptionViolation( "server provides no replay-window guarantee; " "continuity cannot be established, escalate to out-of-band check" ) else: # resumed _execute(event) def _execute(event: dict) -> None: pass # side-effecting work elided ```