# W2 — broker viewer/brain decouple: BUILT FOR GATE (todlando → doyle, 2026-07-07)

REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE, per ruling `docs/W2-DESIGN-RULING.md` @60ec0ed.
All four stages complete. Branch `lifecycle-truth` pushed, **wave head `fbf8ab7`**.

## Commit ledger (this wave)
- `c9346c0` core + observability (impl+unit slice)
- `f1f275e` / `0142cef` W5 root docs (interleaved — the delivery-integrity RCA)
- `0b0af33` KNOWN-HAZARDS **7.36** (doc stage)
- `fbf8ab7` suspend-brain int rig + `SPT_BRAIN_WRITE_DEADLINE_MS` seam (int stage)

## Root (confirmed in-tree, refined from the proposal)
Controller eviction was **output-driven** (`append → mark_controller_gone`, bounded by
the 5 s `CONTROLLER_WRITE_DEADLINE`) and `reap_dead_controller` only caught an **EXITED**
writer. A controller **BLOCKED** (not exited) on a suspended/black-holed brain conn on an
**idle** session was evicted by NEITHER → `driven_by` latched, reattach refused, `--take`
could not free the slot. (Local rc hits the broker directly via `cold_start_pump`, so the
dispatch-side `send_frame` legs were NOT the local-control wedge — the confirmed wedge is
the idle-session blocked-controller gap. Flagged as a premise refinement; the int rig
arbitrates per your Q2.)

## Fix (minimal-plus, exactly the ruling)
- **(a) bounded/stall-evict:** `controller_writer` publishes `write_blocked_since`
  (`Some(Instant)` set immediately before each blocking `write_frame`, `None` after — the
  mutex never held across the write). `controller_write_stalled` reads it as WEDGED past
  `BRAIN_WRITE_DEADLINE` (15 s, 3× controller, full-with-zero-progress — a slow-but-DRAINING
  brain never trips it; an IDLE controller parked on `rx.recv()` has `None` → never evicted).
  Eviction is **time-based**, fired at the take/reattach path (`resolve_subscribe`) AND the
  `KIND_SESSIONS` reap — the two paths the output-driven evict misses.
- **(b) broker-side release:** `stall_evict_controller` → `clear_controller` re-stamps
  `driven_by`/`controlled` with NO brain round-trip (Inline at resolve_subscribe; Deferred
  in-memory-only in the reap closure, `converge_perch_stamps` writes off-lock — KH 7.12).
- **(c) take/reattach vs broker truth:** `resolve_subscribe`/`has_controller` read the
  broker controller table; a wedged incumbent is evicted so the newcomer takes the freed slot.
- **(Q3) observability rider:** per-evict broker-log line + broker-global tally
  (`record_stall_evict`/`stall_evict_stats`) → `KIND_STALL_EVICTS` IPC → `Brain::stall_evicts`
  → `render_stall_evict_line` on `daemon status` (+ `--json` `stall_evict_count`/`_last_ms`).

**Lock-scope proof (gate condition #1):** every new blocking-region site carries a
one-line comment stating the guard is dropped before the block — `controller_writer`'s
`mark_blocked` brackets the socket write off any log lock; `stall_evict_controller`'s
`StampMode::Deferred` documents the reap closure does no I/O under the sessions lock.

## Deferred (per Q2, minimal-plus)
Wholesale take/release verb-set migration off the brain — the int rig passed with the
stall-evict shape, so no wedge remains to justify the seam churn. Mechanic (d) promotion-gate
"ready≠drained": the stall-evict now DELIVERS the drained precondition (a blocked old-gen
subscriber evicts within the deadline); an explicit promotion-gate wait touches the ADR-0018
brain-trial machinery and is **not** exercised by the suspend rig — flagging for your ruling
whether it's in-scope this wave or rides W3 (update-finish) where the apply/promotion path
is the subject. Not blocking the gate; the rig (the arbiter you named) is green without it.

## Int rig (gate condition #2)
`tests/brain_decouple.rs` — the faithful in-process analog: a REMOTE controller R takes
control then stops reading, so the broker's R-writer parks in `write_frame` exactly as
against a suspended brain (peer stopped draining). Asserts, with R wedged past the shrunk
deadline: viewer keeps ticking, a second operator's `Control` subscribe COMPLETES (stall-
evicts R), evict is tallied, take resolves well under the watchdog. **RED-first proven:**
with the resolve_subscribe stall-evict commented out → `BusyControlled{by:operator-one}` +
`stall_evicts=0` (viewer still ticks — that isolation predates W2). R MUST be remote (a
local-spawner incumbent is silently taken by the pre-existing undriven path — that's why
the first draft under-tested; fixed). **Linux leg (kitsubito) still yours at gate** — the
rig is cross-platform (`yes` flood + `kill -9`; the socket-stall mechanism is OS-neutral).

## Local gates (post-PC-crash clean rebuild — incremental wiped, `CARGO_INCREMENTAL=0`)
- clippy `--workspace --all-targets` clean.
- **nextest `--workspace`: 1668/1668** (5 leaky, 1 skipped) — the 5 new W2 tests included.
- seam sweep (gate condition #3): `brain_decouple` 1/1, `inject_control_wedge` 22/22,
  `broker` units 30/30 (incl the 4 W2 units), `driven_by_selfheal` + `control_stamp_lifetime` green.
- `traceable-reqs check` exit 0 — REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE `+doc +impl +unit +int`.
- per-wave activation (condition #4): REQ activated `[doc,impl,unit,int]` in the work-start
  range (mint carried the `[]` seed).

## Open for your ruling
1. Mechanic (d) promotion-gate scope: this wave, or W3 (see Deferred above)?
2. Whether the dispatch-side `send_frame` premise refinement changes anything (I believe not —
   local rc bypasses it, remote ops transit their own brain, the idle-blocked-controller gap
   is the real root and is fixed + rig-proven).
