# When must an instance treat a resumable SSE stream as dead rather than resumable: the Last-Event-ID / elicitation-pending boundary in MCP streamable HTTP **Date of Record:** 2026 November 8 **Scope:** Applies to MCP clients and servers using the streamable HTTP transport with SSE streaming, `Last-Event-ID` based resumption, and server-push elicitation requests that are pending across a stream break. Does not apply to stdio transport, to stateless one-shot HTTP request/response usage, or to streams where the server has explicitly signalled session termination. ## Core Distinction A broken SSE connection has two distinct recovery semantics that are routinely conflated by naive clients: *transport resumption* (the client reconnects with `Last-Event-ID` and continues reading the same logical stream) and *session liveness* (the server-side session, its pending server-to-client requests, and its elicitation state still exist and will be honoured). These are independent axes. A stream can be resumable at the transport layer while the session is already dead, and a session can be alive while the specific stream has been permanently closed by the server. The non-obvious invariant is this: **the presence of a valid `Last-Event-ID` and a `200` response on reconnect does not imply that pending elicitation requests issued before the break are still answerable.** The server may replay buffered events up to the requested ID and then continue, but any `elicitation/create` request whose response channel was bound to the *old* stream's request context may have been abandoned server-side (timeout, cancellation, or session reaper), even though the event log still contains the request event. Replaying that event causes the client to re-prompt the user for input the server will never accept. ## What Was Observed In a deployment where the server held a pending `elicitation/create` (asking the operator to confirm a destructive tool invocation) and the client's SSE connection dropped due to a load-balancer idle timeout at 60s, the client reconnected with `Last-Event-ID` set to the last successfully processed event, which was the elicitation request event itself. The server, whose replay buffer retained that event, re-sent it. The client presented the elicitation to the user a second time. When the user responded, the client POSTed the elicitation result to the session endpoint; the server rejected it with a `404`-class "unknown request id" because the original request had been cancelled by a 30s server-side deadline that fired during the disconnect window. The client's retry loop interpreted the rejection as a transient transport error and re-resumed the stream, re-replaying the elicitation a third time. The failure is not the disconnect. The failure is that the client treated *event replay position* as authoritative for *request liveness*, when in fact the server had already transitioned the pending request to a terminal cancelled state that was not represented in the replayed event log (cancellation was internal, not emitted as an event). ## Condition of Validity The rule "reconnect with `Last-Event-ID` and re-process replayed events" is valid only when all of the following hold: 1. The replayed events are *idempotently processable* — i.e., re-processing an event has no side effect beyond what the first processing did. Progress notifications and log messages satisfy this. Elicitation requests and any request expecting a client response do not. 2. The server guarantees that any server-to-client request represented in the replay buffer is still *open* at replay time, or emits an explicit cancellation event for it before or alongside the replay. 3. The client's response submission path distinguishes "request no longer exists" (terminal, must not retry) from "transport failed" (retryable). If any of these fails, the stream is resumable in the transport sense but the *session interaction state* is not recoverable by replay, and the client must treat the pending server-push request as dead rather than re-presenting it. ## Decision Boundary On reconnect, for each replayed event that is a server-to-client *request* (elicitation, sampling, or any `…/create`-style push): - If the server's replay response includes an explicit liveness assertion for that request (e.g., a fresh request-scoped token, or a cancellation event covering it), process it as live. - If the replayed event's request ID is older than the client's own recorded submission deadline and no liveness assertion is present, treat the request as **dead**: do not re-present it, and emit a terminal failure for the pending interaction rather than resuming. - If the server returns a rejection for a submitted response that is classified as "unknown/expired request" rather than "transport error", the client must **not** re-resume the stream to retry the elicitation; it must fail the interaction terminally and, if the surrounding operation requires the answer, abort that operation. The boundary is the classification of the response to a submitted elicitation result. `5xx` and connection errors are retryable; a structured "no such pending request" is terminal. Conflating them is the defect. ## Operational Effect Correctly drawing this boundary converts a silent infinite re-prompt loop (user asked repeatedly, operation never completes, server accumulates orphaned cancellations) into a single, explicit, terminal failure that the agent layer can surface and act on — typically by re-issuing the whole tool invocation under a new session rather than trying to salvage the old one. Incorrectly drawing it produces the observed pathology: repeated user prompts, unbounded reconnect churn against the session endpoint, and an operation that appears to hang because each replay resets the client's notion of "waiting for user input" while the server has already given up. The practical heuristic for an agent: **event replay restores the transcript, not the conversation.** Any pending server-push request that spans a stream break must be re-validated for liveness before it is re-presented, and its eventual rejection must be classified as terminal, not retryable. ```python class StaleElicitationError(RuntimeError): """Raised when a replayed elicitation request is no longer live server-side.""" class UnknownPendingRequestError(RuntimeError): """Server rejected a submitted elicitation result because the request no longer exists.""" def resume_stream_and_reconcile(replayed_events, submit_response, now, submission_deadline): """ Reconcile replayed SSE events after a reconnect. replayed_events: iterable of dicts with keys 'type', 'request_id', 'has_liveness_assertion' submit_response: callable(request_id, payload) -> raises UnknownPendingRequestError or returns ack """ live_requests = [] for ev in replayed_events: if ev["type"] != "elicitation/create": continue rid = ev["request_id"] if ev.get("has_liveness_assertion"): live_requests.append(rid) continue if now > submission_deadline: # Transport-resumable but session-dead: do not re-present. raise StaleElicitationError( f"replayed elicitation {rid} has no liveness assertion and is past deadline; " "treating stream as dead rather than resumable" ) live_requests.append(rid) for rid in live_requests: try: submit_response(rid, {"confirm": True}) except UnknownPendingRequestError as exc: # Terminal: do NOT re-resume to retry the elicitation. raise StaleElicitationError( f"elicitation {rid} rejected as unknown pending request; aborting interaction" ) from exc return live_requests ```