# releases#289 — bound `request_wan`'s reply read (JIT plan, drafted while held off cargo)

Status: NOT STARTED. Opens on doyle's word after v0.68.0 publishes and release.yml is terminal.
Drafted 2026-09-09 from a READ-ONLY re-read at HEAD. No build has been run for this lane.

## The defect, re-confirmed at HEAD
`crates/spt-daemon/src/wan.rs:283-312`. `request_wan` sends its one-shot record, subscribes, then
reads in an UNBOUNDED `brain.read_event()?` loop. The only non-error exits are a decoded reply and
`NetStreamEof` -> `WanRequestOutcome::NoReply` (:306-307). A peer that ACCEPTS the stream and never
replies, never EOFs, wedges the caller forever. Pre-existing since ec360f16; NOT a #272 regression.

## NEW FINDING from this read — the obvious fix is a NO-OP
The budget precedent is real and stays: `PEER_REPLY_READ_BUDGET` (10s) at
`crates/spt-daemon/src/brain.rs:2507`, with `read_event_until(Option<Instant>)` at :1036 and
`read_peer_reply_until` at :1366 already in place. Do NOT mint a second constant.

BUT `peer_reply_deadline(io_timeout, now)` (:2513) is `io_timeout.map(...)` — it returns **None**,
i.e. UNBOUNDED, whenever `io_timeout` is unset, and that is every non-pump caller. Measured: `wan.rs`
contains ZERO `io_timeout` references, and `request_wan`'s production caller is the CLI send path
(`crates/spt/src/wansend.rs:480`), not the pump. So wiring `reply_read_deadline()` into this loop
would compile, read as a fix, and change nothing on the path the issue is about.
This is the [[unbounded-brain-carrier-cannot-be-bounded]] shape: a caller-side budget that is a no-op.

**Therefore:** derive the deadline EXPLICITLY at the call site — `Instant::now() + PEER_REPLY_READ_BUDGET`
— and pass it to `read_event_until`. Reuse the constant; do not route through the pump-mode helper.
Falsifier for that claim, to run FIRST once cargo is free: a cell that reaches the loop with
`io_timeout` unset must show the helper yielding `None`. If it yields `Some`, this whole section is
wrong and the helper is the right wiring after all.

## Deliverable
1. A BOUNDED read with a DISTINCT outcome — not `NoReply`. `NoReply` means "receiver finished the
   stream without a reply frame" (:186-189, an old/refusing receiver); a budget expiry is a different
   fact and must not be reported as the same one. New variant (name TBD in review, e.g.
   `ReplyTimeout`), threaded through `wansend.rs`'s reporting so a timeout never prints as SENT.
   `from_token`'s unknown-token default stays `NoReply` — that arm is about a token, not a timeout.
2. A cell with a fake peer that ACCEPTS the stream and never replies and never EOFs. Assert the
   bounded outcome AND that it returns inside the budget. Note the shape hazard: assert the
   DISTINCT outcome, not merely "it returned" — returning is what the buggy path does on EOF too.
3. REQ tag in the same commit; `traceable-reqs check` before declaring done.

## Order of work when unblocked
Falsifier cell (helper yields None off-pump) -> variant + bounded read -> fake-peer cell -> treqs.

## Census (read-only, HEAD) — #289 names ONE leg; there are SIX
`wan.rs` carries six one-shot request/reply functions with the IDENTICAL shape — send, subscribe,
then an UNBOUNDED `brain.read_event()?` loop whose only non-error exits are a decoded reply and an
EOF arm:

| fn | loop | EOF arm |
|---|---|---|
| `request_wan` | :295 | yes |
| `request_fork` | :414 | yes |
| `request_redeem` | :499 | yes |
| `request_answer` | :553 | yes |
| `request_knock` | :636 | yes |
| `request_presence` | :680 | yes |

All six are reachable from the CLI (non-pump) side, so `peer_reply_deadline` yields `None` for every
one of them and none is bounded today. The bounded call sites in the tree (`propagate.rs:409`,
`sync.rs:410`, `pump/mod.rs:829`, `webproxy.rs:592/733`, `attach.rs:252`, `digestlink.rs:262`,
`rc.rs:3097`) are all pump-, proxy- or slice-driven.

Precedent for scope: `REQ-PUMP-DIAL-FASTFAIL` fixed `request_update` AND named `request_sync` as the
LATENT TWIN, fixing both in one lane — the twin "bites the moment the want-set is non-empty". Five
latent twins is the same bet, five times. Scope was DOYLE'S call, asked
2026-09-09 — **and RULED the same day: all six. See the ruling section below.** (This line read
"SCOPE IS DOYLE'S CALL, not assumed here" until the ruling landed; kept verbatim it told a reader the
question was still open three paragraphs above its answer.)

## SCOPE RULED (doyle, 2026-09-09): all SIX in this lane, distinct outcome threaded through all six
The middle option is REFUSED, and for the right reason: it would leave five sites reporting a budget
expiry as `NoReply` — the exact defect #289 exists to fix, minted five more times, with a follow-up
filed against ourselves. `REQ-PUMP-DIAL-FASTFAIL`'s latent-twin call is the precedent.

Shape he set, to keep the diff reviewable:
1. ONE deadline helper at the CALL-SITE layer: `Instant::now() + PEER_REPLY_READ_BUDGET` (the
   existing constant — no second constant), called by all six. NOT the pump-mode helper, which
   yields `None` off-pump (see the no-op finding above; falsifier runs first).
2. ONE shared expiry variant for all six. Each caller's report path prints it as its own word —
   never as success, never as `NoReply`. Name must be GREP-ABLE so a reviewer can census all six
   report surfaces in one command.
3. Tests, in order: (a) the falsifier — helper yields `None` off-pump; (b) one unit cell on the new
   helper proving the bound; (c) `request_wan` gets the full cell asserting the DISTINCT outcome;
   (d) the other five get a cell each ONLY if the mock-brain shape gives it in under one cell —
   otherwise they ride the helper cell and the COMMIT BODY names those five as bound-by-construction
   and untested-by-cell, by name. No silent gap.
4. Commit body names all six by file:line, cites #289 for `request_wan` and "latent twin" for the
   five, in the precedent's wording.
5. Scope record: alchemy COMMENT on #289 listing the five twins as in-scope with the one-line
   reason — when the lane opens, not before the release verb and not before the cargo all-clear.

## Execution order (final)
falsifier cell -> helper + unit cell -> variant + threading (6 sites) -> request_wan cell ->
per-twin cells where cheap -> `traceable-reqs check` -> commit naming all six -> alchemy comment.

## COMMIT 1 STATUS (2026-09-09, lane .worktrees/wan289 on build/289-wan-reply-bound)
Code complete, `cargo check -p spt-daemon -p spt --all-targets` CLEAN. Cells running when doyle's
hold landed (hertz timing measurement on this box since 10:15Z, IR-85 arm 1): running cells finish,
NO new cargo until his CARGO CLEAR. Commit is git-only and goes ahead; `traceable-reqs check` is
DEFERRED to after the clear, so commit 1 is COMMITTED-BUT-NOT-DECLARED until it is green.

Files: brain.rs (shared refuse helper + unit cell), digestlink.rs (calls it), wan.rs (PeerSilent +
bounded loop + seam refusal), wansend.rs (variant + classifier + pump carrier at :480), cli.rs
(WAN_PEER_SILENT line), twohost.rs (connect_retry_pump sibling + 4 rungs), twohost_web.rs (:517),
NEW crates/spt-daemon/tests/wan_reply_bound.rs, traceable-reqs.toml (REQ-WAN-REPLY-BOUND).
NOT staged, lane scratch: CHECK1.txt, TESTS1.txt, COMMIT1.msg.

The plan's own carrier finding was WRONG in its first form and is corrected above: a call-site
deadline on the Whole carrier is REFUSED (Unsupported), not ignored. Doyle re-read the four source
claims himself before ruling.
