# ADR-050: `pump_bidi` — the two-pump helper, extracted upstream ## Status Accepted — implemented in alkcall 0.6.0 (review 007 Unit 3 / R-03). Pins alknet ADR-078's two-pump contract in one place. ## Context alknet ADR-078 defined the two-pump data-plane contract for forwarding channels (one pump per direction; each pump shuts the opposite sink down on completion — `try_join!` alone deadlocks) and deferred helper extraction until a second two-pump consumer existed and the shapes converged. Review 007 (R-03) supplied both halves of that test from the alktunnels UDP POC: - **Producer side:** the POC's `pump_halves` — two `tokio::io::copy` pumps over split channel-vs-substrate halves, each shutting down the opposite sink on completion, joined. - **Consumer side:** `TunnelSession::take_halves` + the assembly layer's copy — the same shape modulo channel side. alktty's channels session already implements the shape (its `drive_session_pre_negotiated` awaited inline); alkhttp's ferry does not pump. The shapes converged; the review asked this sweep to either extract the helper or record the decision not to — an un-extracted helper would surface during alktunnels Phase 1 as the same fix→publish→update treadmill for a purely additive change. ## Decision Extract the helper as `channels::pump_bidi`: ```rust pub async fn pump_bidi(channel: C, peer_read: R, mut peer_write: W) -> (u64, u64) where C: AsyncRead + AsyncWrite + Unpin, R: AsyncRead + Unpin, W: AsyncWrite + Unpin, ``` Two pumps, joined: `channel → peer_write` and `peer_read → channel`. On each pump's completion the opposite sink is shut down (shutdown-on-completion — the half-close the contract specifies). Returns the two copy counts for observability. Three deliberate deviations from the review's sketch, all semantics-first: 1. **Return `(u64, u64)`, not `io::Result<(u64, u64)>`.** There is no meaningful `Err` state: both pumps swallow copy errors by contract — a mid-stream error is indistinguishable from an abrupt close (no error channel exists mid-stream, per ADR-049 §6's pinned EOF-shaped semantics), and shutdown-of-the-opposite-sink runs either way. A returned `Err` would be dead code; the counts (with `unwrap_or(0)` on error) are the observability. 2. **Peer side takes split halves (`peer_read: R`, `peer_write: W`), not one `AsyncRead + AsyncWrite` value.** The dominant producer shape is the establisher's dial result: `TcpStream::into_split` halves (tunnels), pty pair halves (TTY). Taking halves matches the POC (`pump_halves`) and avoids forcing consumers to re-join halves just to satisfy a bound. The channel side stays a single value — that is what the pump handler's `accept_bi` yields. 3. **Not `async fn` over `AsyncWriteExt` bounds on both sides.** The channel side needs `tokio::io::split` (one value, two pumps), so its bound is `AsyncRead + AsyncWrite + Unpin`; the `AsyncWriteExt` bound the sketch had on every parameter is the extension trait only where shutdown is called. alktty's three-pump session does not fit the helper (the exit future is a third signal) and stays as-is — the helper serves the two-pump shape, exactly as the review scoped. ## Compatibility Purely additive: a new pub fn in `channels::pump`, re-exported in the module docs; no existing surface changes. Consumers may adopt it at their own pace (alktunnels Phase 1 gets it for free; alktty's channels session can migrate later if it wants; the POC's shape is now canonical). ## Door type **Two-way.** A helper function is trivially removable or re-shapable; no wire surface, no trait, no state. ## Verification gate The POC's two-pump semantics reproduced through the helper, as unit tests in `src/channels/pump.rs`: data flows both directions with exact copy counts; EOF from one side completes the other's shutdown-on-completion (the far end observes clean EOF, not an error); a dead source is EOF-shaped teardown, not an `Err`. ## References - Review 007 R-03 (the extraction ask + the convergence evidence) - alknet ADR-078 (the two-pump contract; shutdown-on-completion) - alktunnels `poc-summary.md` §Issues Surfaced #4 (the producer-side shape; `pump_halves` in the POC's producer.rs) - alktty `src/channels.rs` (the channels session shape; three-pump carve-out) - ADR-049 amendment 2 (R-02 — the lifetime contract the helper's callers must respect: await `pump_bidi(..).await` inline inside the handler task)