HERTZ RCA — same-machine `spt rc --take` leaves two interactive controllers Date: 2026-07-16 Field reproduction: deployah already controlled in terminal A; terminal B ran `spt rc deployah --take`; A stayed attached and continued sending input, but stopped receiving PTY output. VERDICT Confirmed source-level split-brain. Same-machine loopback attaches collapse both terminal windows to the same controller identity. `resolve_subscribe` therefore takes its silent same-identity successor path even for a distinct `--take`: it replaces the broker output sink without sending `Displaced` or closing the old attach stream. The old rc pump remains interactive. PTY input dispatch is session-addressed and does not validate that the sender still owns the controller slot, so terminal A can continue controlling while terminal B exclusively receives rendering. EXACT STATE TRANSITION 1. A owns `OutputLog.controller = { send=A, by=Some(local-node), ... }` through the local loopback attach path. 2. B sends `AttachIntent::Take`, also authenticated as `by=Some(the same local-node)`. 3. `OutputLog::resolve_subscribe`, crates/spt-daemon/src/broker.rs:1296-1318, computes `same_identity = true` and calls `become_controller(B, ...)` as a silent re-take. Intent is not consulted in this branch. 4. The only loud displacement branch is different-remote + `Take`, broker.rs:1334-1347. It queues `Displaced` to the old controller before replacement. Same-node B never reaches it. 5. A receives neither `Displaced` nor stream EOF. Its rc pump therefore remains attached and continues forwarding keyboard input. 6. `dispatch_input`, broker.rs:3920+, accepts `InputReq { session_id, ... }` without proving that the sending connection is the session's current controller. A's input still reaches the PTY. Output fanout now targets B's replacement sink only, so A remains in control but renders no updates: exactly the field symptom. RELATED SOURCE EVIDENCE - `crates/spt-daemon/src/brain.rs:1771-1788` already documents the general defect for `None==None`: silent re-take fires no `Displaced`, orphans the prior local controller, and stops writing its output without closing the connection. - Local rc uses broker loopback (`crates/spt/src/rc.rs:1400-1456`), whose origin is the node identity; two windows on one node are therefore indistinguishable at the current `by` granularity. - rc does handle a real displacement as terminal `PumpEnd::Displaced` (`crates/spt/src/rc.rs:1747-1757`); the broker simply never emits it on this path. INVARIANT At most one input-capable controller lease exists per PTY session. Replacing that lease must atomically revoke the old lease, deliver/close its attach stream, and reject every later Input/Resize from the old lease. Node identity is authorization/attribution, not a unique controller lease identity. FIX SHAPE P0-A — distinct viewport/lease identity: - Mint a unique controller/attach lease ID for each rc invocation/stream; carry it through SubscribeReq/controller slot and Input/Resize. - Keep node identity separately for display/access policy. - Define same-lease + equal/newer generation as the only silent successor/replay case. Same-node but different lease is a distinct controller. P0-B — explicit Take always revokes a distinct incumbent: - If `intent == Take` and the incumbent lease differs, enqueue `Displaced` through the old controller writer, terminate/unsubscribe the old attach stream, then install the taker — regardless of whether `by` node strings match. - Do not key silent takeover solely on `controller_by() == by`. P0-C — broker-enforced input fencing (required defense, not optional hardening): - Bind Input/Resize to the active controller lease or originating broker connection. - Reject/drop commands from every displaced/stale lease after replacement. This is what makes the one-controller invariant true even if displacement notification is delayed or lost. REGRESSION Deterministic two-loopback-client broker test: 1. A subscribes Control from local node N and becomes controller. 2. B subscribes Take from the same node N with a different lease. 3. Assert A receives `Displaced { by: N }` followed by terminal stream completion (or an equivalent explicit revoked event that makes rc exit). 4. Assert output after takeover reaches B and not A. 5. Submit Input and Resize from A after takeover; assert neither mutates the PTY/session size. 6. Submit Input/Resize from B; assert both apply. 7. Assert controlled/driven_by metadata identifies B and only one controller slot exists. 8. Preserve a separate equal-lease/equal-generation replay test proving genuine dispatcher recovery remains silent and does not self-displace. This is not primarily an rc rendering defect. The frozen old viewport is the expected consequence of its sink being replaced; the bug is missing lease identity + missing input fencing + same-node silent-retake policy.