# PUMP-TRUTH W2 — BUILD DELTA (todlando → doyle, 2026-07-09)

Building `REQ-PUMP-PEER-ISOLATION` per `PUMP-W2-BUILD-GO.md`. Two carrier-model
points the design under-pinned surfaced during code-read. Both resolve INSIDE
your rulings (G2 named the fallback) — naming them per "name it if you hit it".
**Not blocking on these** — building the fork-independent layers now; will apply
the carrier resolution below unless you countermand.

## Δ1 — TWO carriers, not one (G2 fallback, TRIGGERED)

Your G2: "one carrier ... split to a dedicated event-drain carrier ONLY if the
impl shows submit backpressure entangling the drain deadline." The trigger is
**stronger than backpressure — it's exactly-once correctness (gotcha #1):**

- The pump's `peer_step` pull legs (sync `request_sync`, update `request_update`)
  BLOCK reading the peer's forwarded reply via `read_peer_reply_until` →
  `read_event_until`, which consumes **every** event kind off the carrier,
  including `NetPresenceEvent`. When a pull-reply read pulls a presence frame it
  doesn't want, `read_event_until` has ALREADY advanced `presence_cursor`
  (`*cursor = ev.seq + 1`) and the caller's `_ => continue` DROPS it. The drain
  loop then never sees that CONNECTED/DIAL_FAILED → **cursor advanced, outcome
  lost** → a peer silently never-rescheduled OR a phantom gap. Single carrier
  breaks the D4c exactly-once property W2 leans on.
- **Resolution:** a second pump-mode brain `events` OWNS the presence
  subscription + drain; the primary `brain` OWNS dial-submit acks + `peer_step`
  IO. Neither swallows the other by construction (separate socket → separate
  reader thread → separate frame channel). This is your named fallback, adopted.

## Δ2 — drain is NON-BLOCKING (`try_recv`), deadline only bounds the submit-wait

The split carrier is a reader-thread + `Receiver<io::Result<Envelope>>`
(`BrainConn::Split`). So the drain reads **available** events via `try_recv`
(new `Brain::drain_presence_now`) — NOT a per-round blocking wait. Round shape:

- submit K non-blocking dials for not-connected / not-backed-off due peers;
- run `peer_step` NOW for already-connected due peers (on `brain`);
- drain: pull all available presence events (`try_recv`); if K>0 submits are
  outstanding, `recv_timeout` up to the **round deadline** = `brain::
  PEER_REPLY_READ_BUDGET + 2s` (symbolic, Δ Q1) for their outcomes; on CONNECTED
  run that peer's due legs in the SAME round; on DIAL_FAILED back the peer off;
  on DISCONNECTED drop its cached conn (NO backoff — Q3);
- `mark_ran` at close. Steady state (all peers cached-connected, K=0) → no long
  wait, cadence unchanged.

## Re-pointed REQ-HAZARD-PUMP-IPC-DEADLINE — BOTH directions, verified path

- **dead BROKER still restarts:** a dead broker closes the IPC conn → the
  `events` reader thread hits EOF → `frames` channel `Disconnected` →
  `read_frame_until` returns an error → `run_peer_pump` returns Err → supervise
  restarts. (The primary `brain` also still bubbles a genuine carrier-op
  `TimedOut` → restart — W1's carrier-desync path, KEPT.)
- **dead PEER never restarts:** its failure is a `DIAL_FAILED` event (bounded by
  the broker's `BROKER_QUIC_OP_TIMEOUT` connect bound) or a reclassified
  reply-read ordinary drop — per-peer drop+backoff, never a round Err.
- `peer_outcome`'s whole-round `?`-poison is DELETED; replaced by a per-peer
  `peer_leg_outcome` (Ok keep · reclassified/ordinary Err → drop+backoff · raw
  carrier `TimedOut` → Err → restart). Int test asserts both directions.

## Semaphore (G4 / Q2)
`Arc<Semaphore>` on `NetHost` (cap 16–32). BOTH `submit_dial`'s spawned task AND
the existing blocking `dial()` acquire a permit (Q2 caveat honored — the cap is
not bypassed).

— todlando
