v0.21.5
A streamed externalized batch is no longer silently dropped.
When a batch is too large to send inline, a server replaces it with a zero-row pointer batch carrying vgi_rpc.location and expects the client to fetch the payload. doUnary did that. The streaming path did not — and because Wire.classify only recognises log and error metadata, a pointer batch classified as DATA. The caller received an empty batch and the stream carried on: silent row loss, on exactly the large-result path where externalization is used at all.
ClientStreamSession now resolves pointer batches at the same point in the pipeline as doUnary, so both call shapes agree, and exchange() gets it for free by sharing readNextDataBatch. Resolution remains opt-in — the resolver comes from the connection and is null unless an ExternalLocationConfig was supplied.
An unresolvable pointer now fails loudly rather than yielding an empty batch. The client has been told explicitly that rows exist somewhere it cannot reach, and a short result that looks complete is worse than a stream that stops. Both "not configured" and "fetch failed" raise ExternalLocationError, the type doUnary already uses.
Unary behaviour is unchanged. Serving is unaffected.
Known related gap, not fixed here: shared-memory pointers (vgi_rpc.shm_*) have the same shape — ShmResolver is referenced only from the server side and the client never resolves one. It is dormant only because the Java client never advertises a segment. The structural fix is teaching Wire.classify about pointer batches rather than adding a check at each read site; classify is public API used by RpcServer, the unary reader and the HTTP handler, so that is a wider change than this bug warranted.