v0.21.6
Exchange streams can be cancelled, and survive a worker error.
Two client-side defects, both found by driving an exchange (scalar-function) stream from a Java client for the first time. Serving a stream was never affected.
cancel() never reached the worker once an exchange had started. A client session is built with EMPTY_SCHEMA as its input schema — the proxy cannot know an exchange's input shape, since the caller supplies it one batch at a time — but IpcStreamWriter defers the schema message and emits the first batch's schema. After one real batch the stream therefore declared the caller's fields while cancel() still built its zero-row token from EMPTY_SCHEMA. The peer's VectorLoader rejected it (no more field nodes for field …), the exception escaped the tick loop, and StreamState.onCancel never fired. The session now tracks the schema the stream actually declared.
A worker error left the session live, corrupting the connection. An error batch is terminal — the server stops its tick loop and writes EOS behind it — but the client threw without closing, and exchange() lacked tick()'s close-on-EOS contract. A caller that caught the error and retried wrote into a transport already back at request dispatch, so the real damage appeared on a later, unrelated call:
poison -> RpcError: poisoned (expected)
after-error -> NoSuchElementException (should be a closed-stream error)
ping -> UndeclaredThrowableException (connection dead)
After the fix after-error reports a closed stream and ping returns normally.
Also adds coverage — not fixes — confirming exchange() inherits the EOS latch and vgi_rpc.location resolution from the shared read path.