You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Harden the listen driver against receive-order races and hostile peers
Move the client's listen-stream demux to a synchronous notification
intercept on the dispatcher's receive path, where cancelled/progress
interception already lives: acks, events, and teardown signals now
advance in wire order relative to the listen result, so a graceful
close can no longer outrun the events that preceded it or clobber the
acknowledged filter with the fabricated empty one. Ack consumption
keys on the live route registry alone (the per-session id set is
gone), route admission enforces the honored filter - loose on URIs,
which the spec lets be sub-resources, with a capped backlog settling
the route lost against floods - and the response-cache eviction
barrier moved to the consumption point, held pending until it
completes so a cancelled consumer cannot lose a level trigger.
On the transport, a request whose SSE stream can never answer - a
non-resumable drop or an exhausted reconnection budget - now resolves
with a synthesized error, contained against teardown races.
Docs: a graceful close is not "stop watching" (servers shed load by
closing gracefully, including this SDK's ListenHandler); the watch
loop re-listens on both endings, and sub.honored is the delivery
contract, not catalog state.
Copy file name to clipboardExpand all lines: docs/advanced/subscriptions.md
+8-6Lines changed: 8 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -88,9 +88,9 @@ Consuming a subscription is one context manager:
88
88
```
89
89
90
90
*`client.listen(...)` takes the filter as keyword arguments — they mirror the wire `SubscriptionFilter` field for field. Entering sends the request and returns once the server's acknowledgment arrives, so `sub.honored` (the subset the server agreed to deliver) is always there before the first event.
91
-
* Iteration yields the same four typed events the server publishes: `ToolsListChanged`, `PromptsListChanged`, `ResourcesListChanged`, and `ResourceUpdated(uri=...)`. An event is a cue to refetch — it carries no payload beyond identity, and duplicates pending consumption collapse into one.
91
+
* Iteration yields the same four typed events the server publishes: `ToolsListChanged`, `PromptsListChanged`, `ResourcesListChanged`, and `ResourceUpdated(uri=...)` — where the URI may be a sub-resource of one you subscribed to, at the server's discretion. An event is a cue to refetch — it carries no payload beyond identity, and duplicates pending consumption collapse into one.
92
92
* Leaving the block ends the subscription, with the transport's own spelling: over streamable HTTP the request's response stream is closed (that is the 2026 cancellation signal), on stream transports `notifications/cancelled` is sent.
93
-
* The stream's two endings are control flow. The server closing gracefully simply ends the `async for`; an abrupt drop raises `SubscriptionLost`. There is no replay and no automatic re-listen — a client that reconnects refetches what it depends on:
93
+
* The stream's two endings are control flow. The server closing gracefully simply ends the `async for`; an abrupt drop raises `SubscriptionLost`. The distinction is diagnostic — a clean end versus a connection worth suspecting — not a difference in what to do next: either way the stream is gone, nothing is replayed, and a watcher that still cares re-listens and refetches. Servers close streams gracefully for their own reasons — shutdown, or shedding a subscriber whose backlog grew past bounds, as this SDK's `ListenHandler` does — so a graceful close is not a signal to stop watching:
# Graceful close or abrupt drop, the stream is gone either way. Back
106
+
# off before re-listening - a graceful close may be the server
107
+
# shedding load, and reconnecting instantly recreates the pressure.
108
+
await anyio.sleep(1)
107
109
```
108
110
109
-
* Checking the acknowledgment (the spec's client SHOULD) is reading `sub.honored` — for example, `if not sub.honored.prompts_list_changed:` the server has no prompts to watch. Multiple subscriptions may be open concurrently; each demultiplexes by its own subscription id.
111
+
* Checking the acknowledgment (the spec's client SHOULD) is reading `sub.honored` — the kinds this stream will actually receive. A server may narrow the filter it agrees to honor (a multi-tenant server declining a URI, say), and `sub.honored` is that delivery contract — it says nothing about what exists in the catalog. Multiple subscriptions may be open concurrently; each demultiplexes by its own subscription id.
110
112
* Tool calls and other requests run freely beside an open stream — from the same task between events, or from sibling tasks sharing the client. A watcher task that refetches inside its event loop is the intended pattern, not a re-entrancy hazard.
111
113
*`listen()` requires a 2026-07-28 connection and raises `ListenNotSupportedError` on older ones, steering to the deprecated `subscribe_resource` and `message_handler` spelling those wires use.
0 commit comments