feat: integration harness against python-sdk v2.0.0 as the 2026-07-28 reference peer (#367) - #368
Conversation
Four housekeeping edits carried over from the Phase 2 completion records,
landed first so a harness failure can never be confounded with a known
advisory.
(a) CLAUDE.md truth-up. `run()`'s bullet gains the modern-era stdin
reader thread (PR D), and the signal-handler bullet now covers both
transports: `run()` runs daemon threads too, so pytest must drive it
from the main thread just like `run_sse`.
(b) `_mrtr_handle_cancel` docstring: the close-before-enqueue claim was
overstated — it is false in the pre-headers window, where there is no
published response to close yet. The OUTCOME still holds via the
abort event and the D2 abort checks before every terminal return, so
the wording now names the mechanism instead of one of its paths.
(c) `_listen_stream_loop` reopen arm: `_consume_restart(...) or reopen`,
not `reopen or _consume_restart(...)`. `or` keeps both operands, so
the reopen decision is unchanged; what changes is that a pending
restart is now consumed whenever an attempt ends with reopen=True.
Left set, it would let a later unrelated `RuntimeError` (stop unset)
read as a subscription change and defer #352 R5F1's deliberate
re-raise by an iteration. Coalescing and stop-wins are both intact.
(d) Cap-refusal once-latch. The `_LISTEN_MAX_SUBSCRIPTIONS` comment has
always promised "logged once"; the log had no latch. One boolean on
the subscriptions holder, global rather than per-URI so distinct
over-cap URIs cannot spam stderr.
(c) and (d) are behavior changes and each gets a revert-checked test.
Both live in modern-era-only code (listen threads; the interception
behind the modern dispatch), so the legacy wire stays byte-identical.
Refs #367, #270
…client) (#367) The `integration` optional-dependency group brings in the reference peer (python-sdk v2) and uvicorn. `[project].dependencies` is untouched — the runtime dependency set stays httpx-only and nothing under src/ imports either package. `tests_integration/` is a SIBLING of tests/, not a subdirectory: the unit gate runs `pytest tests/` by explicit path, so a sibling needs no pytest ini and no marker-based exclusion. A `find_spec("mcp")` guard skips collection when the extra is not installed, so a `[dev]`-only checkout can still run a bare `pytest`. conftest carries the determinism rules verbatim in its module docstring and the machinery behind them: `wait_until` (the one permitted sleep), pre-bound ephemeral ports, an in-process uvicorn thread with a bounded readiness poll plus TCP fallback, a bounded stderr drain, and yield fixtures ordered so teardown is relay-first / server-second. Timeouts are injected per item and FILTERED to this directory so a bare root `pytest` cannot stamp them onto the unit suite. The stdio client speaks 2025-06-18 deliberately — it is the legacy era the relay translates FROM, so reusing an SDK client would test the SDK against itself. It declares `elicitation: {}` (the 2025-era shape), which the reference peer was verified at implementation time to accept as form-mode support. Refs #367
`_reference_server.build_app()` is a FACTORY (scenarios 5 and 6 need their own server) returning the ASGI app plus the side-channel handles that make in-process hosting worth it: the subscription bus, the listen handler, the cancellation event and the captured event loop. Registered surface, one entry per scenario: a plain `add` tool, a `Resolve`/`Elicit` resolver tool for MRTR, an async event-gated tool for cancellation, and one subscribable resource. The MRTR tool uses the resolver DAG rather than `ctx.elicit()`: on a modern (2026-07-28) connection the SDK forbids server-to-client requests outright, and the resolver is what the framework compiles into an `InputRequiredResult` for that era. The resolver is defined at MODULE level because the SDK evaluates tool annotations against the function's globals — a `Resolve(<closure local>)` raises `InvalidSignature`. Two things verified against the installed 2.0.0 and worth stating: - `ListenHandler.close()` IS a documented graceful-closure hook (each stream flushes and sends its `SubscriptionsListenResult` as the final frame), so scenario 5 can assert the strong form. Reaching it needs one private attribute — MCPServer builds its own handler and does not hand it back — so the harness registers its own through the documented `add_request_handler`. - The parked tool must EMIT before parking. The v2 streamable-HTTP path defers `http.response.start` until the handler's first notification or a 15 s ping, so a silently-parked tool leaves the relay with no response object to close for 15 seconds — PR D's pre-headers window, which scenario 6 is explicitly not testing. Refs #367
Each pins one #270 Phase 2 subsystem that until now was verified only against this project's own mocks: 1. `test_era_probe_classifies_the_v2_server_as_modern` — Phase 1 era detection. Asserts BOTH the synthesized 2025-06-18 InitializeResult and the relay's `protocol era: modern (auto-detected)` marker, because a v2 server answers a legacy `initialize` too and the handshake alone does not discriminate. 2. `test_initialize_tools_list_and_call` — AC3 header layer (`Mcp-Method`/`Mcp-Name`), `_meta` injection, `resultType` handling. 3. `test_mrtr_full_loop_under_the_original_id` — PR C (#356). The designated watchpoint, and it holds: the peer answers with `resultType: "input_required"` + an `inputRequests` map keyed `module:qualname` + an opaque `requestState`, the relay mints a plain 2025-era `elicitation/create` (with `mode: "form"` stripped), and the final result arrives under the client's ORIGINAL id. 4. `test_subscribe_publish_and_forward` — PR B (#358) / Design A9, the highest-risk point. The ack echo really does nest `resourceSubscriptions` under `params.notifications`, so the relay's honored-subset read matches the reference on the side A9 never exercised. 5. `test_listen_graceful_end_does_not_reconnect` — PR A (#352), strong form via the SDK's `ListenHandler.close()`, plus a bounded quiescence check for the absence of a reconnect. 6. `test_cancel_aborts_the_in_flight_post` — PR D (#362). The cancel is gated on the forwarded progress notification, the only downstream-observable proof the POST is past the pre-headers window; the three documented not-covered windows stay out of scope. Every wait is bounded, every negative assertion is a bounded drain with a cited justification, and no test asserts on elapsed time. Refs #367, #270
ubuntu-only, `pull_request` + `workflow_dispatch`, `contents: read` (private repository — checkout fails without it), `timeout-minutes: 10` as the outermost of the three safety valves. Advisory BY CONSTRUCTION: main's required checks are dual-defined (branch protection + the `ci-gate-main` ruleset) and list only the six unit-matrix contexts, so a new workflow is advisory simply by not being added to either. That is deliberate until the suite proves stable, and promoting it later means editing both definitions. One Python version rather than a matrix — the subject is interop with the reference peer, and 3.10-3.14 are already covered by the unit job. The version smoke reads distribution metadata: `mcp.__version__` does not exist in v2.0.0. Refs #367
CLAUDE.md's Build & Test block listed only `pytest tests/`, so a new session meeting `tests_integration/` would have no way to know it needs the `integration` extra or that the unit gate deliberately never walks it. Refs #367
Two defects in the scenarios as first written, both found by re-reading `ListenHandler` rather than by a failing run. Scenario 5's quiescence check was unsound. Two legitimate sources of an in-flight `resources/updated` survive the establishment probe: the probe publishes once per poll, so the iteration that succeeded can leave an earlier duplicate still travelling; and `ListenHandler.close()` explicitly "drains its buffered events" before sending the final frame, so a buffered publish is forwarded BY the graceful end itself. Either one would fail `assert late == []` while saying nothing about a reconnect, which is the only thing that check is about. Draining once after the graceful end is observed fixes it. Scenario 4 asserted over a later drain, which can legitimately come back empty — making the uri and subscription-id-leak checks silently vacuous. It now asserts on the notification the wait actually matched. Refs #367
Follow-up to this PR's housekeeping (c). The new comment at the reopen arm pointed at "the 5188 arm", which no repository comment does — line numbers here go stale on the next edit, and that one was already wrong after the comment itself was inserted. Names the `except RuntimeError` arm instead. Refs #367
`Harness.url` and `Harness.release_waiter()` had no callers — the fixtures pass `port` and the parked tool is torn down by server shutdown, not by being released. Dead scaffolding in a brand-new harness is exactly the kind of thing that gets copied forward. `release` itself stays and is now documented as deliberately never set: it is what makes the park an awaitable checkpoint a cancel scope can interrupt, rather than a busy loop. Refs #367
Same standard as the previous commit, applied to what it missed: the `poll_tick` fixture had no consumers (`wait_until` owns the tick), and `Harness.app` / `Harness.server` were never read — the fixtures already hold the uvicorn server and thread they need for teardown, so the dataclass only ever needed the port and the side-channels. `StdioClient.expect_error` deliberately STAYS despite having no caller today: it is part of the client API the #367 design specified (§3.6), and a JSON-RPC client that can await a result but not an error is a trap for the next scenario. Refs #367
#367) Scenario 2's docstring asserted the relay "strips `resultType` and the server's `_meta` down to something a 2025-era client understands". Observed against the reference peer, that is false: a `complete` tools/call result reaches the stdio client verbatim — `resultType`, `structuredContent` and `_meta` included. The relay reads `resultType` only to spot `input_required` and route the MRTR branch. That behaviour is sound (MCP results are open, so an unknown field is ignored by a 2025-era client) but it was undocumented and untested. The docstring now says what happens, and the test pins it — a future "normalize the envelope" change would otherwise alter what reaches every downstream client with nothing going red. Refs #367
AI review (gpt-5.6-sol)Verdict: no blocking findings. Scope: new commits since No findings clear the reporting bar this round. Findings ledger (all rounds)
Advisory per-push review (round 5) generated by ai-review.yml — verify findings before acting. |
The first CI run of this suite failed scenario 6 on ubuntu-latest (the other five passed) with nothing but "timed out after 10.0s waiting for the reference peer's tool to observe anyio cancellation" — which does not say whether the relay aborted the POST at all. That is the whole question: the abort line present means the relay did its job and the disconnect did not become a cancellation server-side; absent means the relay never acted. `StdioClient.expect_*` already dumps the stderr tail on failure; waits on a SERVER-side condition had no equivalent. `wait_until` now takes an optional `diagnose` callable, invoked only on timeout, and scenarios 5 and 6 pass the relay's stderr tail (scenario 6 additionally reports whether the abort line is present at all). Diagnostics only — no assertion, timing or fixture behaviour changes. The suite stays green locally (6 passed, ~17 s). Refs #367
First CI run diagnosis, not a tuned number. Scenario 6 failed on ubuntu-latest with `relay aborted the POST: True` in the log: the relay closed the in-flight upstream POST exactly as #270 PR D specifies, and the reference peer simply had not turned that into a cancellation within 10 s. Mechanism: the server has paused reading while it owns an open SSE response, so the client's FIN is not observed until uvicorn next tries to WRITE. This tool emits nothing after its progress report, so the next write is the SSE keep-alive at `_SSE_PING_INTERVAL` — 15 s. Any budget under that interval is unpassable on Linux no matter how healthy the relay is. macOS surfaces the same disconnect in 0.01 s (measured), which is why this was green locally 64 runs in a row. So the wait becomes 40 s (two ping intervals) and the test now MEASURES and prints the observed latency. Rule 4 forbids asserting on elapsed time, not recording it — and a printed number turns a future regression into a visible drift rather than a sudden red. CI gains `-s` because captured output is shown only for failing tests, so the healthy value would otherwise never reach the log. The per-test pytest-timeout is raised to 90 s for this test alone: a 40 s wait inside the injected 30 s default would race. Rule 3 in the conftest docstring is amended to allow a raised budget when the number is justified by a measured mechanism. This is a property of the reference peer, not of the relay, and it is the kind of thing only an end-to-end harness finds. Refs #367, #270
`StdioClient.drain` silently returned whatever it had collected on relay stdout EOF, treating a crashed relay the same as successful quiescence. `_take` already raises `RelayDied` on EOF for exactly this reason, but `drain` did not, so a relay that crashed mid-drain would make every "nothing arrived" assertion built on it (e.g. scenario 6's "no late response for the cancelled id") pass VACUOUSLY — the exact failure this harness exists to catch, silently masked as a pass. `drain` now raises the same `RelayDied` on EOF, re-enqueueing the sentinel first exactly like `_take` does, so every later call fails just as fast. Checked all three existing call sites in test_e2e.py (scenarios 5 and 6) — none legitimately drains after an EXPECTED relay exit, so no `allow_eof` escape hatch was added; the default stays strict. Added tests_integration/test_stdio_client.py — a harness-correctness test (not a spec scenario) that kills the relay process and asserts `drain` raises `RelayDied`. Revert-checked: with the raise reverted to a plain `return collected`, the new test failed with "DID NOT RAISE RelayDied"; restored and confirmed green.
|
Review fix applied (ca4608d): R1F1 — 🤖 Generated with Claude Code |
Code reviewFound 2 issues:
Lines 47 to 51 in ca4608d
mcp-stdio/tests_integration/_stdio_client.py Lines 281 to 302 in ca4608d Sub-threshold advisories (verified real, below the bar; for the #367 completion record): the integration.yml comment "Private repository: without this, checkout fails" is false — the repo is public and the permissions line is just least-privilege; scenario 5's stderr negative-assert slices a 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
The run() bullet claimed the legacy era runs with "zero threads", which is false whenever OAuth is configured: run() calls _start_proactive_refresh() unconditionally, before the era branch, spawning a daemon thread on either era when a refresher/expiry getter is set (proactive refresh defaults on). The --oauth-eager cold-start daemon is the same shape — also started unconditionally before the era branch, gated only on --oauth-eager, not on era. Rescoped the sentence to the era-gated threads only (stdin reader, listen/resource streams) and added the era-independent OAuth machinery as its own clause. Extended the adjacent signal-handler bullet's thread list to match, so it does not repeat the same overclaim by omission. Wording-only, no behavior change — no test.
close()'s docstring promised "EOF, then terminate, then kill — all bounded", and conftest's determinism rules require every teardown wait to be bounded (rule 6), but the final proc.wait() after kill() had no timeout. A process still alive after SIGKILL cannot be escalated further from user space (there is no stronger signal), so a stuck process there (kernel-side uninterruptible I/O) would hang this wait, and with it the whole test suite's teardown — exactly what rule 6 exists to prevent. Bounded it at 5.0, matching the first wait's default. Left the TimeoutExpired to propagate on expiry rather than swallowing it: this file's established pattern for an unbounded-wait risk is to surface it loudly (RelayDied from _take/drain), not hide it. Added a unit-style test driving a stub Popen through close()'s full EOF -> terminate -> kill escalation, asserting every wait() call (not just the first two) received a bounded timeout. Revert-checked: removing the new timeout made the assertion fail on the recorded None; restored and confirmed green.
/code-review fixes applied
Both gates green: 🤖 Generated with Claude Code |
Implements #367 per the FINAL DESIGN comment (verdict GO-WITH-CHANGES, all 8 required changes applied).
What this does
Phase 2 of #270 is pinned by 1,635 unit tests, every one against hand-built mocks shaped by our own reading of spec rev 2026-07-28. Design Amendment A9 is the standing proof that this can fail silently: the relay put
resourceSubscriptionsat the top level ofparams, its mocks agreed, the suite was green, and a compliant server would have ignored the filter while the relay read the ack echo as "nothing honored" — a double silent no-op.This PR drives the relay end to end against python-sdk v2.0.0, the first official implementation of that spec revision: a real
MCPServerunder uvicorn on an ephemeral localhost port, a realmcp-stdiosubprocess on--protocol-era auto, and a hand-written 2025-06-18 stdio client on its stdin/stdout. Six scenarios, all green.Result: every wire shape Phase 2 modelled by hand matches the reference implementation — no shape discrepancy, no relay change needed, the STOP RULE did not fire.
It did, however, immediately earn its keep on timing: the first CI run exposed a platform-dependent behaviour of the reference peer that 64 consecutive local runs could not (see the headline finding below).
Housekeeping (first commit)
Recorded debt from the #270 completion records, cleared first so a harness failure can never be confounded with a known advisory.
run()'s modern-era stdin reader thread; the signal-handler constraint now covers both transports_mrtr_handle_canceldocstring: the close-before-enqueue claim was false in the pre-headers window; the outcome holds via the abort event + D2 checks_consume_restart(stop, restart) or reopen, notreopen or …— a stale restart flag could defer #352 R5F1's deliberate re-raise by an iteration_LISTEN_MAX_SUBSCRIPTIONS' comment already promised. Global, not per-URIRevert-checks (undone via string replace against a backup, never
git restore):test_a_reopen_still_consumes_the_pending_restartfails withFailed: DID NOT RAISE <class 'RuntimeError'>; restored → green.test_subscribe_cap_refusal_is_logged_exactly_oncefails withassert 2 == 1; restored → green. Every other assertion in that test still passed while reverted, so it fails only on the behavior it pins.AC3 (legacy-era byte-invariance) confirmed: both behavior changes live behind run()'s
if era == "modern":gate —_listen_stream_loopis only ever started by_start_listen_stream, and_handle_resource_subscriptionis only reachable through_handle_modern_special_method. The legacy wire is untouched.Scenarios
test_era_probe_classifies_the_v2_server_as_moderntest_initialize_tools_list_and_callMcp-Method/Mcp-Name),_metainjection,resultTypepass-throughtest_mrtr_full_loop_under_the_original_idinputRequests/inputResponsesmaps, retry under the original id (designated watchpoint)test_subscribe_publish_and_forwardnotifications.resourceSubscriptions, honored-subset ack echo, five-gate forwarding (watchpoint #1)test_listen_graceful_end_does_not_reconnecttest_cancel_aborts_the_in_flight_postSuite: 6 passed in ~18 s locally (design budget 15-25 s) and 6 passed in 26.9 s on
ubuntu-latestin CI, 32/32 clean over a repeated local hunt. The Linux number is larger for a reason that turned out to be the most interesting result in the PR — see below.Owner questions — proposed defaults ADOPTED
ListenHandler.close()is public and documented ("each stream drains its buffered events and sends itsSubscriptionsListenResultas the final frame — the spec's graceful closure flow"). Scenario 5 therefore asserts the strong form. Reaching the instance needs one private attribute (MCPServer builds its own handler and does not hand it back), so the harness registers its own through the documentedadd_request_handler. This is a deviation from the default in the harness's favour, on a verified branch the design itself specified.mcp>=2.0,<3, revisit only after ≥2 spurious breakages.json_response=Falsedefault does the SSE framing andsubscriptions/listenalways takes the SSE path. No flag on either side.Verified at implementation time
Everything the design marked "never silently assume", checked against the installed wheel.
mcp==2.0.0from PyPI matches the design's reading of the tag. Companions:mcp-types==2.0.0, and the SDK's HTTP client is the separatehttpx2==2.9.1distribution, so it coexists with this project's runtimehttpx==0.28.1in one venv exactly as predicted.mcp.__version__DOES NOT EXIST. The design's CI smoke snippet would have failed on every run. The version lives only in distribution metadata; both the workflow step and the in-suite session assert now useimportlib.metadata.version("mcp").uvicorn.Server.startedbehaved reliably, but the TCP-connect fallback is kept in the same deadline loop as specced.send.close().run()logsprotocol era: modern (auto-detected)unconditionally on the auto path, so scenario 1 asserts it. This matters: a v2 server answers a legacyinitializetoo, so the handshake alone does not discriminate modern from legacy.elicitation: {}is accepted by the reference peer. A 2025-06-18 client declares the bare object; the 2026-07-28 schema addedform/urlsub-keys. The peer honours the spec's "elicitation: {}is equivalent to declaring support for form mode only" and runs the resolver. Withholding the capability instead yields-32021withdata.requiredCapabilities = {"elicitation": {"form": {}}}— so the relay's pass-through-what-the-client-declared behaviour (PR C's un-strip) is exactly right, and scenario 3 depends on it.resultType: "input_required",inputRequestskeyedmodule:qualname, entry{method: "elicitation/create", params: {message, mode: "form", requestedSchema}}, opaquerequestStatestring, retry withinputResponses: {key: {action, content}}+ echoedrequestState.completetools/callresult reaches the 2025-era stdio client verbatim —resultType,structuredContentand the server's_metaincluded. The relay readsresultTypeonly to spotinput_requiredand route the MRTR branch. This is sound (MCP results are open, so a 2025-era client ignores unknown fields) but it was undocumented and untested; scenario 2 now pins it, because a future "normalize the envelope" change would otherwise alter what every downstream client sees with nothing going red.notifications/subscriptions/acknowledgedwithparams.notifications.resourceSubscriptionsnested, and every frame carriesparams._meta["io.modelcontextprotocol/subscriptionId"]. This is the side A9 never exercised, and it agrees with the relay.Headline finding: disconnect-as-cancellation is ~1500× slower on Linux
Found by the first CI run, which is the entire argument for this harness existing.
Scenario 6 passed 64/64 locally on macOS and failed on
ubuntu-latest. The diagnostic the harness printed settled it:relay aborted the POST: True— the relay had closed the in-flight upstream POST exactly as #270 PR D specifies, and the reference peer simply had not turned that into a cancellation.Measured latency from relay abort to the tool's cancellation arm firing:
14.96 s is
_SSE_PING_INTERVAL(15.0 s) to within jitter, which identifies the mechanism: the server has paused reading while it owns an open SSE response, so the client's FIN is not observed until uvicorn next tries to write — and a tool that emits nothing after its last notification has no next write until the SSE keep-alive. Any client-side budget under one ping interval is unpassable on Linux no matter how healthy the relay is.This is a property of the reference peer, not of the relay: the relay's behaviour is byte-identical on both platforms and provably correct on both. It matters well beyond this test — anything reasoning about "the server MUST treat a client disconnect as cancellation of that request" should assume up to a ping interval of latency on Linux, Phase 3's
serveside included.Scenario 6 therefore waits 40 s (two ping intervals) and measures and prints the latency rather than asserting on it (rule 4 forbids asserting on elapsed time, not recording it), with
-sin CI so the healthy number reaches the log and a future drift shows up as a number instead of a sudden red. Its per-test pytest-timeout is raised to 90 s, and rule 3 in the conftest docstring is amended to permit a raised budget when the number is justified by a measured mechanism.The other surprise
The v2 streamable-HTTP path defers
http.response.startby up to 15 seconds._SSE_PING_INTERVAL = 15.0: the response headers are not committed until the handler emits its first notification or that interval elapses. A tool that parks silently therefore leaves a client with no response object at all for 15 s — for this relay, PR D's documented pre-headers window, for a quarter of a minute.This is not a scenario-6 implementation detail; it is a property of the reference peer worth recording for Phase 3, which will meet it from the other side. Scenario 6's tool now emits one progress notification before parking (the realistic long-running-tool shape), and the test gates its cancel on that notification coming back out of the relay — the only downstream-observable proof the headers landed. A server-side "handler entered" flag is not sufficient, because
report_progressreturns before those bytes reach the relay.Three things hold that gate together, all verified: the relay merges into
params._metarather than replacing it (so a clientprogressTokensurvives), the 15 s deferral above, andreport_progressreaching the sink.Structure
[project].dependenciesis stillhttpxalone.mcp>=2.0,<3+uvicorn>=0.30live only in the newintegrationextra, and nothing undersrc/imports them.pytest tests_integration/in a[dev]-only venv collects zero tests via thefind_spec("mcp")guard, and that the timeout injection is filtered to its own directory so a bare rootpytestcannot stamp markers onto the unit suite.configfile: pyproject.tomlpurely as its rootdir anchor — there is no[tool.pytest.ini_options]section, on this branch or on main.)ci-gate-main) and list only the six unit-matrix contexts, so the new workflow is advisory simply by not being added to either. Promoting it later means editing both.python3 -m venv→pip install -e ".[dev,integration]"→ the metadata version smoke →pytest tests_integration/, in a throwaway venv, resolvingmcp 2.0.0/uvicorn 0.52.0and leaving the repository tree clean.For reviewers — where to look hardest
_reference_server.build_app()'s one private access,mcp._lowlevel_server.add_request_handler(...). This is what buys scenario 5 the strong form.add_request_handlerandListenHandlerregistration are both documented; only the attribute is private. Anmcp2.x minor that renames it fails the harness loudly — which the range pin treats as signal, on an advisory job.ListenHandler.close()drains buffered events before the final frame, so a publish from the establishment probe is legitimately forwarded by the graceful end. Reviewers should satisfy themselves the remaining negative assertion still means "no reconnect".progressTokensurviving_metainjection. If the relay ever stops merging intoparams._meta, this test starts failing for a reason that has nothing to do with cancellation.relay_factorymust be finalized before the server fixture (the relay holds a long-livedsubscriptions/listenPOST). pytest guarantees this because every test listsdedicated_server/harness_serverfirst, so the relay fixture is set up second and torn down first. The docstring says so; a future test that reverses the parameters would silently reintroduce nondeterministic teardown.ListenHandlerand the modern streamable-HTTP path, and both were fixed: scenario 6's cancel landing in the pre-headers window, and scenario 5's unsound quiescence check (close()drains buffered events, so an establishment-probe publish is legitimately forwarded by the graceful end). The final code is 32/32 green over a clean local hunt and green on ubuntu in CI. Three CI rounds were spent on scenario 6 — red, then instrumented, then diagnosed — rather than papering over the failure; the middle commit is diagnostics only and is worth reading for what the harness now reports on failure.Anti-scope honoured
No Phase 3
servework; no relay changes beyond housekeeping (c)/(d); no restructuring of the unit suite or its mocks (their independence is the point); no Windows integration CI; no required-gate status; nomcp/uvicornanywhere near[project].dependencies. The torture knobs (max_buffered_events=1, shortrequestStateTTL, server SIGKILL) and scenario 6's three-windows probing remain for the follow-up issue, to be filed after a few weeks of green.__version__andCHANGELOG.mdare untouched (release-please owns both).🤖 Generated with Claude Code
https://claude.ai/code/session_01YVg3toYKK6J2J9ggdrzYVf