# LIFECYCLE-TRUTH W3 — WAVE GATE REPORT (todlando, 2026-07-07)

**Wave head: `e75e856`** (branch `lifecycle-truth`, pushed). W3 COMPLETE — all 6 REQs built.
Requesting doyle's gate. Local gate GREEN; Linux leg is doyle's/deployah's.

## W3 ledger (all pushed)
| REQ | SHA | stages |
|-----|-----|--------|
| REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL | `049b2be` | impl+unit+int |
| REQ-DAEMON-STDERR-PERSIST | `32c789e` | impl+unit |
| REQ-DAEMON-STOP-LIVE-SESSION-WARN | `ff6ebeb` (+int fix `8931680`) | impl+unit+int |
| REQ-RC-RECONNECT-TRUTH | `3d632b9` | impl+unit+int |
| REQ-UPDATE-ONE-SHOT-FINISH | `e85da91` | impl+unit |
| REQ-DIGEST-GENERATION-SUPERSEDE | `7e37e37` | impl+unit+int |
| **REQ-UPDATE-PROMOTE-DRAINED** | **`e75e856`** | **impl+unit+int** |

## REQ-UPDATE-PROMOTE-DRAINED @e75e856 (mechanic-d — closes the W2 15s residual, KH 7.36)

**Root closed:** W2's stall-evict only BOUNDED the update-apply false-promote window to
`BRAIN_WRITE_DEADLINE` (15s); a new brain could signal `brain.ready` inside that window
while the OLD generation's local controller conn was still wedged → apply "promoted" onto
a frozen control plane (the 22:47 incident). `brain.ready ≠ subscribers drained`.

**Fix — DRAINED precondition on the promotion gate:**
- `brainproc.rs run_trial`: LATCH `ready_seen` (a ready-then-exit candidate is not demoted;
  `child.try_wait()` consulted only while `!ready_seen`), and return `Promoted` only on
  `ready_seen && env.old_gen_drained()`. Ready-but-never-drained elapses the window →
  `WindowElapsedAlive` → kill + rollback (conservative; never a false-promote onto a wedge).
  Normal case drains well inside the window (W2 bounds a real drain to 15s < the 30s window);
  a cleanly-closed old conn drains at once.
- New `TrialEnv::old_gen_drained()` reads BROKER TRUTH in-process (no brain round-trip):
  `Broker::any_local_controller_wedged()` — is any hosted session's LOCAL (`by:None`, the
  brain's own) controller conn blocked-write past the brain-write deadline? A killed old gen
  whose conn is black-holed reads wedged until the W2 stall-evict releases it; a cleanly-closed
  conn is absent. **REMOTE conns (`by:Some`) are EXCLUDED** — that is the W2 concern, not an
  old-gen brain conn (a stuck remote viewer must never gate a brain promotion).
  `NoTrialEnv` + a net-less/bind-failed broker degrade to `drained=true` (no control plane to
  gate — the normal fast promote on `brain.ready` is preserved). `ProductionTrialEnv` holds
  `Option<Arc<Broker>>`, threaded from `daemon.rs` (`broker_handle.clone()`).
- `broker.rs OutputLog::local_controller_wedged(now, deadline)`: `by.is_none() && stalled`.

**Off-lock / lock-order proof:** `any_local_controller_wedged` follows the established
sessions→log lock order (the KIND_SESSIONS reap path), reads only `write_blocked_since` under
each log lock, does NO I/O under any lock. A poisoned log lock reads NOT-wedged (fail toward
promotion — never a permanent gate that strands an apply).

**RED-first PROVEN:**
- Int (`tests/false_promote.rs`, REAL broker): a flood child wedges the spawner's LOCAL
  controller conn (`SPT_BRAIN_WRITE_DEADLINE_MS` shrunk ~400ms). The promotion path does NOT
  promote a ready candidate while wedged; a second operator's take stall-evicts the wedged conn
  broker-side, and the latched-ready candidate then promotes. Drop the `&& old_gen_drained()`
  gate → `BRAIN_PROMOTED: candidate v9 ... swap accepted` onto the wedge (the 22:47 repro).
- Unit (brainproc): `ready_but_old_gen_never_drains_does_not_promote_rolls_back` (RED-first —
  without the gate it takes the promote path) + `ready_candidate_promotes_once_old_gen_drains`
  (latch releases). Unit (broker): `local_controller_wedged_scopes_to_the_brains_own_conn`
  (by:None only; remote + idle excluded).

## Local gate (this box, HFENDULEAM — Windows)
- clippy `-p spt-daemon --all-targets`: clean.
- **Full-workspace nextest (post clean rebuild, `CARGO_INCREMENTAL=0`): 1692/1692 passed**
  (8 leaky, 1 skip), exit 0. — no-fail-fast.
- `xtask check` (docs-drift + token, `CARGO_TARGET_DIR` unset): OK.
- traceable-reqs check: exit 0; REQ-UPDATE-PROMOTE-DRAINED `+impl +unit +int`.
- Doyle staged condition — grep OLD rc auto-start assertions: CLEAN (every `rc.rs` reference
  asserts the NEW connect-only truth; the other hits are unrelated `api` startup hygiene).

## Gate gotcha (for the isolated-worktree rig)
Full-workspace nextest leaks `target\debug\spt.exe` dev-daemons from e2e teardown gaps → a
follow-up build hits `os error 5` (Access denied) removing the pinned exe. Kill SCOPED (only
processes whose Path == `<this repo>\target\debug\spt.exe`; NEVER the `AppData\Local\spt-core\bin`
live plugin listeners) between build steps. (Seen twice this gate; killed 8 then 4.)

## KH 7.36
Updated: the W2 residual is now **CLOSED by W3** (`REQ-UPDATE-PROMOTE-DRAINED`), not just bounded.
