Brings in "Stop answering cancelled requests" and reapplies its policy
onto the rebuilt seam instead of the code it targeted:
- The cancelled-request answer policy now lives at the correlator's
single site: a peer-cancelled request is never answered by the dispatch
layer, its late result or error is dropped, and it settles through a
transport-supplied hook. The dispatcher forwards the hook it receives in
message metadata; the streamable HTTP transport supplies its own, which
ends the request's stream with the REQUEST_CANCELLED terminal error
written through the request's ordered channel (so a resuming client's
replay terminates too).
- Hook containment happens once, in the correlator.
The second review round found several defects that traced back to a few
structural gaps rather than isolated bugs; this addresses the gaps.
- Channel delivery is now a value: write() returns whether the message
reached the event store or an attached response, an EventStore failure is
contained inside the write (resumability degrades, the stream survives,
no store text on the wire), and attach() returns None on a dead channel
so nothing can stream from one. A server-to-client request that cannot
reach any client fails the caller with CONNECTION_CLOSED instead of
parking it, and the JSON-response body derives from the channel's
recorded outcome (result, terminated-404, or 500) rather than a
"cannot happen" branch.
- Stream ids handed to the EventStore are minted by the transport in a
session-scoped namespace, so a client-chosen request id can no longer
name the standalone GET stream and two sessions on one store can no
longer replay each other's frames.
- One SSE-response runner owns the pump, error containment, and cleanup
for the POST, GET, and replay responses (the POST path had lost the
guard its siblings kept).
- A session-level gate holds requests that arrive while an initialize is
still being served until the handshake commits, restoring the ordering
the stream-pair driver's parked read loop used to guarantee.
- The POSTed client message is delivered even when the 202 could not be
written back, and the correlator marks the single site where the
cancelled-request answer policy lives for every transport.
Adds regression tests for each of the above.
- Serialize store-then-forward per channel: concurrent writers on one
channel (the standalone GET stream) could put frames on the wire out of
event-store order, breaking Last-Event-ID resumption. A per-channel lock
restores the store-order == wire-order invariant the serial router used to
provide.
- Refuse a request that arrives across session termination instead of
running it: dispatch now branches on the transport's identity (stateful
vs stateless) rather than on session-task presence, re-checks liveness
after the request's awaits, and answers 404 for an ended session; queued
work for an ended session is dropped.
- Contain event-store failures at the channel: a raising store_event was
reaching the correlator as a handler error and leaking its text onto the
wire; write() now never raises (a broken store ends that stream), which
also protects the courtesy-cancel write.
- Close the replay reader when priming fails; drop dead exception arms in
the SSE pump.
- Correct the migration note's JSON-mode claim to request-scoped requests,
drop a claim handlers cannot observe, fix a vacuous test assertion, and
add regression tests for each fix.
Records the removal of StreamableHTTPServerTransport.connect() (the
transport is now driven per request by the session manager) and the
behaviours clarified alongside it, and refreshes the docstrings that still
described the old serve_loop wiring.
Removes now-dead code (the attach take-over branch, redundant containment
around the notification handler, an unreachable None-connection arm, the
channel-closing loop in run() teardown) and adds tests for the behaviours
that were untested: terminating a session with a request in flight,
client-posted progress for a server-initiated request, containment of a
raising event store per request, concurrent POSTs sharing a request id, the
channel attach/detach identity guard, and the serve_loop driver.
The per-session message router and its stream fan-out are gone, so the
tests pinning that mechanism are rewritten against the behaviour they
guarded (priming-store failure returns 500 with no leaked state; standalone
stream teardown via close_standalone_sse_stream logs no error; the manager
evicts sessions whose task exits or crashes; stateless requests leave no
channels behind) or deleted where the guarded race is now unrepresentable
(#1764 router head-of-line blocking, the standalone writer between-dequeues
window). The close_sse_stream protocol-version gating tests move to the
renamed metadata builder.
Python's $ with re.match also matches just before a single trailing newline, so tool-name validation accepted "name\n" and UriTemplate.parse accepted varnames like "foo\n". Switch both checks to re.fullmatch.
Closes#3084