packages/core/src/state/session.ts:289-300 in applyFeatures carries a comment that says "v1.1 §6.2 — runtime is authoritative for advertised features; the welcome MUST advertise only what is in both lists", but the implementation does not intersect. When the runtime advertises a features array, the welcome copies it verbatim (out.features = runtime.features); the client's features is consulted only as a fallback when the runtime did not advertise. The neighbouring applyEncodings at packages/core/src/state/session.ts:264-280 does exactly what the comment describes — it intersects clientEncodings.filter((e) => runtimeEncodings.includes(e)) when both are present — so the divergence is clearly a bug in applyFeatures, not an intentional design.
The downstream effect is that a client that does not implement a v1.1 feature still sees it advertised in session.welcome. Any subsequent runtime behavior gated on "client opted in" will fire against a client that cannot honor it, producing protocol errors that the negotiation step was meant to prevent.
Fix prompt: change applyFeatures to mirror applyEncodings: when both client.features and runtime.features are defined, set out.features = runtime.features.filter((f) => client.features!.includes(f)) (or symmetric — the spec asks for the intersection, order doesn't matter for correctness). Keep the fallback paths for the cases where only one side advertised. Add a packages/core/test/state-session.test.ts case that covers (runtime only), (client only), (both with overlap), (both disjoint → empty array), and (neither set → undefined), asserting the welcome's capabilities.features is the intersection or the single-sided value.
packages/core/src/state/session.ts:289-300inapplyFeaturescarries a comment that says "v1.1 §6.2 — runtime is authoritative for advertised features; the welcome MUST advertise only what is in both lists", but the implementation does not intersect. When the runtime advertises afeaturesarray, the welcome copies it verbatim (out.features = runtime.features); the client'sfeaturesis consulted only as a fallback when the runtime did not advertise. The neighbouringapplyEncodingsatpackages/core/src/state/session.ts:264-280does exactly what the comment describes — it intersectsclientEncodings.filter((e) => runtimeEncodings.includes(e))when both are present — so the divergence is clearly a bug inapplyFeatures, not an intentional design.The downstream effect is that a client that does not implement a v1.1 feature still sees it advertised in
session.welcome. Any subsequent runtime behavior gated on "client opted in" will fire against a client that cannot honor it, producing protocol errors that the negotiation step was meant to prevent.Fix prompt: change
applyFeaturesto mirrorapplyEncodings: when bothclient.featuresandruntime.featuresare defined, setout.features = runtime.features.filter((f) => client.features!.includes(f))(or symmetric — the spec asks for the intersection, order doesn't matter for correctness). Keep the fallback paths for the cases where only one side advertised. Add apackages/core/test/state-session.test.tscase that covers (runtime only), (client only), (both with overlap), (both disjoint → empty array), and (neither set → undefined), asserting the welcome'scapabilities.featuresis the intersection or the single-sided value.