Skip to content

feat(transport): share one response-tag composer and add a targeted response sender - #109

Merged
ContextVM-org merged 1 commit into
ContextVM:mainfrom
harsh04044:feat/cep8-targeted-response-sender
Aug 19, 2026
Merged

feat(transport): share one response-tag composer and add a targeted response sender#109
ContextVM-org merged 1 commit into
ContextVM:mainfrom
harsh04044:feat/cep8-targeted-response-sender

Conversation

@harsh04044

Copy link
Copy Markdown

Part of #100

Sixth piece of CEP-8, after the client negotiation in #108. No payments code in it: this is the transport primitive PR 8's gating middleware needs, plus a refactor that had to land before it.

Two changes, in that order. send_response and the CEP-41 deferred publish each wrote out the same outbound tag policy by hand, so that's now one function both call. Then send_targeted_response, which publishes to a specific client and request event without consuming the correlation route, so a middleware can answer a request without ending it.

What's here

  • compose_response_tags: routing, CEP-35 discovery replay, CEP-8 effective-mode disclosure, cap pricing. Private, synchronous, takes values the caller already resolved under its own session lock.
  • send_response and publish_open_stream_deferred_response routed through it. send_response's tags binding is now immutable, which turns "append before you measure" from a comment into a compile error.
  • send_targeted_response, plus targeted_response_sender returning the same publish as a closure for callers with no &self.
  • TargetedResponseSender, modeled on the existing PublishFrame alias.
  • 13 tests: 11 unit, 1 in transport_integration.rs, 1 end to end through a real client.

No Cargo.toml change, no new dependency, no FFI change.

Why it's built this way

  • The refactor lands first. send_response and the deferred publish are the two copies that have to stay identical by hand, and all four of PR 4's revision rounds were that hand-sync failing. The targeted sender would have been the third copy. Doing it first was the deal when it got deferred.
  • The no-op is verified on the wire. Built main in a worktree, published the same responses through both, diffed the events. Byte identical on both paths across plain, first-response discovery, the advertisement-collision case, a capability-list result, and oversized. That last one carries the most risk: the start frame keeps the full composed list and every continuation frame keeps only p and e.
  • It leaves event_routes alone. The sender has to be callable by a caller that answers without dropping the request. Consume the route there and the real response later fails correlation and the client hangs. The seam already pops the route when a middleware drops, so a gated request's route still gets released.
  • An error response still replays the discovery set. The client latches its session baseline on the first inbound event carrying any tag outside p/e, so a -32042 carrying the disclosure but no discovery set makes a stateless client latch a thin baseline and report no server name for the rest of the session. That's PR 4's round-2 bug on a new path. ts lands in the same place through buildOutboundTags.

Heads up

The closure captures the announcement tag sets when you build it, not when you call it. Build it after setting those tags, or every targeted response goes out with empty discovery and empty pricing and nothing complains at compile time. There's a test pinning the difference and a tracker line for PR 9.

Three deliberate differences from ts. We pass pricing through the same shape gate routeTargeted skips, which is unobservable in practice since the gate only fires on a result and every gating message is an error. We mirror the inbound gift-wrap kind where routeTargeted doesn't, so an ephemeral request doesn't get answered with a relay-stored wrap; ts disagrees with itself here because route() does mirror, and it's worth raising upstream. And the response carries the client's original JSON-RPC id where ts carries the event id, because the two SDKs rewrite on opposite sides of their middleware seam. Both clients correlate on the e tag, so neither is misled.

A targeted response is never fragmented, matching routeTargeted, which has no oversized branch either. Filed as a follow-up next to the identical gap on the CEP-41 deferred path.

Scope is transport and not payments because nothing here is payments-specific. The changelog entry still sits in the CEP-8 group.

Gate is green: fmt, clippy -D warnings, 847 tests under --all-features and 685 under --no-default-features, doc, the FFI C suite against an empty diff, MSRV 1.88, plus CEP-22, CEP-41 and e2e_happy_path by name. 23 mutations cut at the composer, at all three callers and at both sender forms, all killed.

@harsh04044 harsh04044 changed the title feat(transport): share one response-tag composer and add a targeted r… feat(transport): share one response-tag composer and add a targeted response sender Aug 12, 2026
@ContextVM-org
ContextVM-org merged commit faa4abe into ContextVM:main Aug 19, 2026
10 of 11 checks passed
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.

2 participants