- N-1 (Important): m3's BadNodeIdUnknown throw for an ack with no resolvable
condition counted toward the host's AlarmAcknowledge breaker (threshold 5), so
re-acks of one unresolvable condition blocked every genuine ack. It now logs a
Warning and returns; only a no-reply transport failure throws. Test through the
real CapabilityInvoker: six unresolvable acks, then a genuine ack that is still
delivered (red with the throw restored).
- N-2: ShutdownAsync holds _subscriptionGate for the whole teardown (in-flight
subscribes finish first, bounded by the budget), so a subscribe arriving during
the delete phase finds the session released instead of creating a subscription
on the closing session. Red-tested.
- N-3: the shutdown-side cert-validation detach is generation-checked under
_probeLock with the map / endpoint clear.
Refs #743
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
- Every InitializeAsync/ShutdownAsync bumps a lifecycle generation. The session,
namespace map and endpoint are installed under _probeLock only while the init's
generation is current, so an init the host abandoned that finishes after the next
shutdown discards its session instead of resurrecting it. Every health write
(lifecycle, keep-alive, publish-error, read path) is generation-stamped, so a late
writer can no longer overwrite newer state.
- O-2: the init catch closes and disposes the LOCAL session it opened (it closed the
still-null Session property, orphaning an activated session + keep-alive timer).
- O-9: ShutdownAsync spends ONE budget (4 s, inside the host's 5 s PostStop budget) on
wire calls: every subscription delete concurrently, then the session close with
what remains; waits are abandoned when the budget fires even if the SDK ignores the
token. It used to be 5 s per subscription, serially.
Refs #743
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT