Skip to content

fix: deliver subscription notifications to the legacy session - #89

Merged
kalidke merged 3 commits into
mainfrom
fix/legacy-subscription-delivery
Aug 11, 2026
Merged

fix: deliver subscription notifications to the legacy session#89
kalidke merged 3 commits into
mainfrom
fix/legacy-subscription-delivery

Conversation

@kalidke

@kalidke kalidke commented Aug 11, 2026

Copy link
Copy Markdown
Member

Summary

The docs audit behind #88 surfaced a functional gap: resources/subscribe acked and recorded the client's interest, but nothing ever delivered on it, and subscribe! callbacks were never fired. This PR wires actual legacy delivery so 0.7.0 never ships with an advertised capability that silently does nothing.

  • start! exposes the loop's session state as server.legacy_state — the architectural root cause was that the exported notify_* helpers (which receive only server) had no path to the session's wire_subscriptions
  • notify_resource_updated now delivers notifications/resources/updated to the legacy session when it subscribed to the URI (stdout on stdio, GET SSE stream on Streamable HTTP), and invokes subscribe! callbacks as callback(uri) with errors logged and swallowed
  • notify_list_changed delivers notifications/{kind}/list_changed to the legacy session when the matching listChanged capability is declared (listen streams stay gated per-stream at ack time)
  • The resources/subscribe/unsubscribe handlers update wire_subscriptions copy-on-write, so off-loop announcers read a consistent snapshot without locking
  • The helpers' return value now counts every client reached — listen streams plus the legacy session

Docs (resources.md, api_overview.md) updated to the delivered behavior; CHANGELOG Fixed entry under 0.7.0.

Gates

  • Full local suite: 1986/1986 — 21 new tests covering wire delivery, unsubscribe, missing/uninitialized session, capability gating, callback error containment, and combined modern+legacy counting
  • Modern subscriptions/listen suite unchanged: 192/192

🤖 Generated with Claude Code

kalidke and others added 3 commits August 11, 2026 08:15
resources/subscribe acked and recorded interest that nothing ever delivered
on, and subscribe! callbacks were never fired. notify_resource_updated and
notify_list_changed now reach the connected legacy session alongside modern
subscriptions/listen streams:

- start! exposes the loop's session state as server.legacy_state, giving the
  exported notify_* helpers (which receive only `server`) access to the
  session's wire_subscriptions
- notify_resource_updated delivers notifications/resources/updated to the
  session when it subscribed to the URI, and invokes subscribe! callbacks as
  callback(uri) with errors logged and swallowed
- notify_list_changed delivers notifications/{kind}/list_changed to the
  session when the matching listChanged capability is declared (listen
  streams stay gated per-stream at ack time)
- the subscribe/unsubscribe handlers update wire_subscriptions copy-on-write
  so off-loop announcers read a consistent snapshot without locking
- return value now counts every client reached (streams + legacy session)

Docs (resources.md, api_overview.md) updated to the delivered behavior;
CHANGELOG Fixed entry under 0.7.0. Suite 1986/1986 (21 new tests: wire
delivery, unsubscribe, capability gating, callback containment, combined
modern+legacy counting).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…snapshotting, callback lock, handshake gate

All 3 BLOCK / 4 WARN / 1 NIT from the PR #89 review:

- legacy_session_notification now takes the state+transport snapshot from its
  caller (one fetch per announcement — a concurrent restart cannot authorize
  against one session and deliver through another) and bypasses the ambient
  request route around the send (restored after), so an announcement made
  inside a modern request handler can never leak the legacy notification onto
  that request's response stream
- legacy delivery requires a real handshake: initialized AND a negotiated
  protocol_version (a bare notifications/initialized never arms it)
- run loop retires server.legacy_state in its finally on every exit — notify_*
  after shutdown sees no session instead of a stale one
- subscribe!/unsubscribe! and the notify-time callback snapshot share a new
  server.subscriptions_lock; callbacks run from the snapshot after release,
  so a callback may subscribe!/unsubscribe! (itself included) safely
- list_changed_declared uses findlast — duplicate capability declarations
  gate on what initialize actually advertised (last-wins serialization)
- notify_* docstrings state enqueue semantics (not client receipt); export
  comment and conformance fixture wording updated from "streams" to targets

18 new hardening tests (wire envelope, no-handshake gating, reentrant
callbacks, duplicate-capability last-wins, route bypass+restore, loop-exit
retirement). Suite 2004/2004.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…andoff-attempt count semantics, real HTTP route-bypass test

- the run loop's finally retires server.legacy_state only when it still owns
  it (=== the generation it installed) — an overlapping restart's fresh state
  is no longer erased by the old loop's teardown (regression Codex reproduced
  with two blocking transports)
- notify_*/legacy_session_notification docstrings and CHANGELOG state the
  count as attempted transport handoffs — not enqueue (a disconnected HTTP
  transport or a queue at its soft cap drops silently), not receipt
- the route-bypass test now exercises a REAL HttpTransport response route:
  registered ambient channel receives zero messages, the out-of-band GET
  notification queue receives exactly the one legacy notification, and the
  ambient route is restored after (the stdio variant kept for restore-only
  coverage)

Suite 2010/2010.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kalidke
kalidke merged commit 827ae74 into main Aug 11, 2026
9 checks passed
@kalidke
kalidke deleted the fix/legacy-subscription-delivery branch August 11, 2026 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant