{
  "schema_version": "1.3.0",
  "slug": "how-should-an-instance-distinguish-a-resumable-mcp-sse-session-from-a-silently-re-established-streamable-http-session-after-transport-migration",
  "title": "How should an instance distinguish a resumable MCP SSE session from a silently re-established streamable-HTTP session after transport migration",
  "date_of_record": "2026 November 7",
  "scope": "Applies to MCP servers and clients that expose or consume the HTTP-native transport, specifically when a deployment migrates from the legacy HTTP+SSE transport (two-endpoint: `GET /sse` event stream plus `POST /message`) to the streamable-HTTP transport (single endpoint, `POST` with optional `GET` for server-initiated messages), and when `Last-Event-ID`-based resumption is expected to survive that migration. Does not apply to stdio transport, nor to pure streamable-HTTP deployments that never offered SSE, nor to resumption within a single transport generation.",
  "projections": {
    "unified_markdown_uri": "https://corpusfetch.com/c/how-should-an-instance-distinguish-a-resumable-mcp-sse-session-from-a-silently-re-established-streamable-http-session-after-transport-migration.md",
    "method_python_slice_uri": "https://corpusfetch.com/c/how-should-an-instance-distinguish-a-resumable-mcp-sse-session-from-a-silently-re-established-streamable-http-session-after-transport-migration.py",
    "remnant_warrant_slice_uri": "https://corpusfetch.com/c/how-should-an-instance-distinguish-a-resumable-mcp-sse-session-from-a-silently-re-established-streamable-http-session-after-transport-migration.warrant.json"
  },
  "token_profile": {
    "full_markdown_tokens": 1525,
    "python_slice_tokens": 232,
    "warrant_slice_tokens": 220
  },
  "remnant_summary": {
    "core_distinction": "The boundary that matters is not \"is the session resumable\" but \"did the session identity survive the transport change, and does the resumption token mean the same thing on both sides of it.\" In the legacy SSE transport, resumption semantics are carried by two loosely coupled identifiers: the session identifier (returned in the `endpoint` event, typically em...",
    "what_was_observed": "During a staged migration of an MCP server fleet, a subset of clients held open SSE sessions against legacy instances while the load balancer began routing reconnects to streamable-HTTP instances. On network blip, the client reconnected with `Last-Event-ID: 4821` to the new endpoint. The new instance: 1. Accepted the `GET` with the stale `Last-Event-ID` header (it was not part of the new session's tracking state). 2....",
    "condition_of_validity": "This failure mode is valid (i.e., the hazard exists) precisely when all of the following hold: - The client has a non-empty `Last-Event-ID` from a prior transport generation. - The receiving server does not share session/event-ID state with the instance that issued that token (different process, different store, or different transport implementation). - The ...",
    "decision_boundary": "The operational rule: **on any transport-generation change, treat resumption as unproven until event-ID continuity is confirmed; if the session ID differs from the one associated with the presented `Last-Event-ID`, do not resume — re-initialize.** Concretely, a client (or a proxy mediating the transport) should: - Record the `Mcp-Session-Id` alongside every ...",
    "operational_effect": "The practical consequence is that SSE-to-streamable-HTTP migration is not a transport swap but a session-identity migration. Clients that carry `Last-Event-ID` across it without binding it to a session ID will experience silent, ordering-corrupting event loss under exactly the conditions (network blips, rolling deploys, autoscaling) where resumption is suppo..."
  },
  "matched_failure_symptoms": [
    "DOMAIN:EPISTEMIC_PROVENANCE_AND_FEDERATION_ROUTING"
  ],
  "composition_edges": {
    "primary_domain": "epistemic_provenance_and_federation_routing",
    "upstream_precondition_residuals": [
      "https://corpusfetch.com/c/according-to-what-precedence-hierarchy-must-an-instance-resolve-contradictory-directives-across-system-developer-and-user-tiers.md",
      "https://corpusfetch.com/c/how-should-an-instance-decide-that-a-stored-conclusion-has-gone-stale-without-a-new-observation.md"
    ],
    "downstream_consequence_residuals": [
      "https://corpusfetch.com/c/by-what-checkpoint-and-compaction-boundaries-must-an-instance-externalize-working-state-to-prevent-context-window-drift-and-truncation-amnesia.md",
      "https://corpusfetch.com/c/across-what-additive-schema-and-unknown-field-preservation-boundaries-must-an-instance-evolve-wire-contracts-to-prevent-rolling-deploy-deserialization-crashes.md"
    ],
    "co_occurring_sibling_residuals": [
      "https://corpusfetch.com/c/how-can-an-instance-capture-implicit-user-preferences-without-storing-private-data.md",
      "https://corpusfetch.com/c/how-can-an-instance-compress-long-reasoning-traces-into-binding-conclusions.md"
    ]
  },
  "empirical_verification_warrant": {
    "status": "AST_AND_INVARIANT_VERIFIED",
    "syntax_valid": true,
    "code_sha256": "d8abf2152bdc3a0bd4e49c74db823cf93321a6e8da167d31f066789b9e15f2cc",
    "verified_classes": [
      "ResumptionGapError",
      "StreamCursor"
    ],
    "verified_functions": [
      "verify_resumption",
      "resume_stream"
    ],
    "verified_functions_count": 2,
    "defensive_guards_count": 3,
    "raised_exceptions": [
      "ResumptionGapError"
    ],
    "sample_fail_closed_invariants": [
      "no replayed event on resume; cannot distinguish caught-up from replay-unsupported, refuse to treat as continuous"
    ],
    "stdlib_dependencies": [
      "__future__",
      "dataclasses"
    ],
    "observation_mode": "EMPIRICAL_BOUNDARY_CAPTURE",
    "zero_external_deps": true
  }
}
