Channel 0 was dead in both directions (C-01): the call protocol's
stream-per-request model (open_bi per call) is incompatible with
channel 0's single yield-once BiStream — every call_open_op failed
with StreamClosed on the connect side, and channel 0's Connection was
a black hole on the accept side.
The fix is single-stream call mode (ADR-036 amendment): all
EventEnvelope frames are multiplexed on channel 0's one BiStream.
Changes:
- CallConnection gains single_stream_writer: Option<Arc<SharedFrameWriter>>
and new_single_stream() constructor. call_with_payload,
subscribe_with_payload, publish_with_payload, and abort branch on
is_single_stream() — in single-stream mode they write frames through
the shared writer (mutex-serialized) instead of opening a fresh
open_bi per call.
- Dispatcher::run_loop_single_stream reads frames off channel 0's read
half, dispatches call.requested, writes responses through the shared
writer, and routes in-flight call.published/call.completed/
call.aborted to the matching Pub sink's chunk_tx by request_id.
- ChannelsAdapter::handle's InstallChannelZero hook now receives
channel 0's Connection (built by the adapter) and runs the
single-stream dispatch loop on it — closing the accept-side black
hole. Mux runner is spawned BEFORE install_channel_zero so
mux.register(0) can complete.
- ChannelClient::from_connection uses CallConnection::new_single_stream
and spawns a read pump (read_single_stream_until_closed) that routes
channel-0 response frames into the PendingRequestMap via
dispatch_envelope — closing the connect-side StreamClosed path.
- ADR-036 amendment records the single-stream-mode decision (two-way
door: implementation detail, wire format unchanged).
Acceptance gate (C-25 #1): three end-to-end tests wire
ChannelClient ↔ ChannelsAdapter over a real tokio::io::duplex pair
carrying the channels 8-byte chunk header wire format:
- channel_0_end_to_end_call_round_trip: a Query op round-trip
- channel_0_end_to_end_unknown_op_returns_not_found: NOT_FOUND
- channel_0_end_to_end_publish_delivers_chunks: a Pub op with 3 chunks
Verification: 437 tests pass (was 434; +3 e2e), clippy clean, fmt
clean, doc warnings unchanged (4, pre-existing C-09).
Consolidates three code-review passes (one main summary + sub-review A
on ADR-046 + sub-review B on channels ADRs 034–043/047) into a single
verified document under docs/reviews/. Every finding was re-verified
directly against the source at f305f8c with exact file:line refs.
Findings (all verified): 10 critical, 19 major, 7 minor.
- Pub end-to-end cannot work (chunks written to a different bi-stream
than the request); publish() takes Vec<Value> not a Stream; 30s
client timeout; abort-doesn't-cancel-Pub; dispatch re-implements
invoke_sink inline; from_call sink forwarding swallows errors.
- Channel 0 dead in both directions; register_openable absent;
resolve_channel_manager stub; backpressure drops chunks; buffer cap
in messages not bytes; ledger decrement on 1/4 teardown paths; demux
desyncs on oversized chunk; no channel-adoption/collision scheme.
- channel/control + resources/subscribe stubs; busy-wait spin; lost
EOF sentinel; TOCTOU on max_channels; ~50 lines of abandoned
deliberation in poll_write; cargo doc 4 warnings; spec docs
inconsistent post-047.
Includes a 6-unit remediation plan sequenced by dependency, with
acceptance gates. Units 1/2/4 need no spec decisions; Units 3 and 5
each need one written ADR decision first (ADR-047 §4 env-resolution;
§5 channel-id adoption). The overarching acceptance gate is the
end-to-end ChannelClient ↔ ChannelsAdapter test whose absence let
both commits land green.
Two corrections to the original reviews noted in the verification log:
- C-17 (lost EOF sentinel): Sub Review B overstated "the pump does not
write an EOF chunk" — the pump does write EOF when it receives the
sentinel; the real bug is narrower (sentinel lost on full buffer →
pump exits on recv→None without writing EOF).
- Warning count: main review said 5 cargo doc warnings; this pass sees
4 (2× register_openable, 1× default_policy, 1× env module/macro).
Verification:
- cargo test → 432 passed (no source changed; docs-only commit)
- cargo clippy --all-targets -- -D warnings → clean
- cargo fmt --check → clean
- cargo doc --no-deps → 4 warnings (documented as C-09; not addressed
here — fixing them requires the register_openable method to exist,
which is Unit 3 of the remediation plan)
The call protocol had Subscription (server→client streaming) but lacked
the directional complement: client→server streaming, where the initiator
produces a stream and the responder's handler consumes it. This gap was
inherited from the @alkdev/pubsub EventEnvelope prior art, which has
subscribe but no wire-level publish.
ADR-046 adds the Pub primitive:
- OperationType::Pub (client→server streaming)
- OperationType::Subscription renamed to Sub (wire: "subscription" → "sub")
- SinkHandler type + HandlerKind::Sink variant
- PublishStream type alias (Stream<Item = Result<Value, CallError>>)
- OperationRegistry::invoke_sink() dispatch path
- call.published wire event (sixth event type, additive)
- OperationSpec.publish_schema (Option<Value>, validates per-chunk input)
- DispatchResult::Sink + SinkDispatch (handler future + chunk channel)
- Dispatcher::pump_sink (feeds call.published chunks from wire to handler)
- CallConnection::publish() / publish_with_payload() client methods
- from_call sink forwarding handler (make_sink_forwarding_handler)
- make_sink_handler() helper
Fan-out/broker (one producer, N consumers, topic matching) is deferred to
the channels session — the call protocol is point-to-point; the broker is
a routing concern that sits above it. The Pub primitive is the
load-bearing piece the broker will compose on.
- 23 new tests (366 total, up from 343)
- clippy clean, fmt clean
Verification:
cargo test — 366 passed
cargo clippy --all-targets -- -D warnings — clean
cargo fmt --check — clean