{
  "summary": "Root cause is deterministic for two `rc` processes originating on the same node. `--take` is parsed and transmitted correctly, but broker ownership collapses every operator viewport on a node to the same `by` identity. In `OutputLog::resolve_subscribe` (`crates/spt-daemon/src/broker.rs:1296-1318`), `same_identity` is handled before `AttachIntent`; every equal-or-newer generation silently calls `become_controller` and returns `Controller`. Therefore a same-node `Take` never reaches the only loud-displacement arm at `1335-1347`. `become_controller` drops the incumbent output sink, so the original `rc` immediately stops receiving PTY output, but it does not close the original attach stream or revoke its target-side `serve_attach` worker. No `BrokerEvent::Displaced` is produced, hence no `AttachRecord::Displaced` or FIN reaches the original pump. The new `rc` attaches and renders, while the original process remains alive. More seriously, the original serve worker's local `role` remains `Controller`, and its input path (`attach.rs:470-490`) still calls generic `send_effect_no_ack`; `dispatch_input` (`broker.rs:3920-3988`) does not validate that the sending broker connection is the current controller. Thus broker output-controller truth is singular (the new sink), but both attach workers can still inject keystrokes: the apparent dual controller includes a real dual-input defect. No edit or test was run, per the read-only constraint; the deterministic failing seams are identified below.",
  "files": [
    {
      "path": "crates/spt/src/cli.rs",
      "description": "CLI intent derivation is correct. `Cmd::Rc` declares mutually exclusive `--view`/`--take` at lines 185-197; dispatch calls `rc_intent` at 1426-1428; `rc_intent` maps view→Viewer, take→Take, default→Control at 1718-1727. The defect is downstream, not clap or flag ordering."
    },
    {
      "path": "crates/spt/src/rc.rs",
      "description": "Operator lifecycle. `run_attach_inner` at 1131-1211 lets Take bypass the client-side `current_driver` Control-only refusal (1136-1147). `establish_attach` at 1417-1528 resolves local session first: local uses `net_dial_loopback` and a real session id (1451-1457); remote uses registry/QUIC, placeholder session 0, and `endpoint_id` (1458-1480); both converge on `request_attach_endpoint` (1514) and subscribe. `pump` at 1840-2024 sends input (1918-1925), clean-detaches by FIN (`detach`, 1869-1874), consumes `AttachRecord::Displaced` (1995-1998), and treats displacement as final (1693-1707). The loud terminal copy is emitted at 1271-1277. If it never receives Displaced, it cannot exit as displaced. A bare target-side FIN after output would be classified `Severed` and trigger reconnect (`serve_eof_end`, 1711-1724), so a fix must preserve a terminal displacement reason before FIN rather than merely cutting the stream."
    },
    {
      "path": "crates/spt-daemon/src/attach.rs",
      "description": "Attach protocol bridge and stale-worker input hole. `handle_attach_request` at 237-269 passes the handshake/loopback `origin_node` as `by`, plus request `gen`, into `Brain::attach_as`. `serve_attach` owns separate session-receive and wire-forward broker connections (315-341). Subscribed Controller/TookControl sets a worker-local `role=Controller` at 517-528. Input is authorized only by that stale local role and then sent through generic broker input at 470-490; it is never revalidated after takeover. Correct displacement handling exists at 549-555: forward `AttachRecord::Displaced`, FIN the operator stream, return `AttachServeOutcome::Displaced`. Clean operator FIN handling is 580-596: resting Detach first, then generation-validated `detach_session_gen`. Request construction at 658-690 makes the stream ConnectionBound and stamps `gen=now_ms()`; input/resize writers are 694-726."
    },
    {
      "path": "crates/spt-daemon/src/broker.rs",
      "description": "Primary defect. `OutputLog::become_controller` (991-1068) drops/replaces only the incumbent ControllerSink/writer, bumps epoch, starts the new writer, and stamps control; it does not terminate the incumbent attach stream/serve worker. `resolve_subscribe` (1263-1352) computes `same_identity` solely from node-valued `by` at 1296; for same identity, only an older nonzero gen is refused (1308-1313), while equal OR newer gen silently replaces at 1315-1318 without examining `intent`. The only `Take`/Displaced arm is restricted to a DIFFERENT remote at 1335-1347. Persisted stamps are node+boolean only (`stamp_driven_by`, 1450-1473), so replacing one same-node viewport with another writes identical `driven_by`/`controlled` values and cannot expose the orphan worker. `detach_if_gen` (1395-1417) correctly prevents the old generation's later FIN from clearing the new slot. `controller_writer` (1671-1773) deliberately drains queued terminal control frames after supersession, but loud displacement is only best-effort: `old.tx.try_send(CtrlMsg::Control(...))` at 1342-1344 ignores Full/Disconnected. If the old queue is full or its writer is wedged, even a different-node take may replace the sink without delivering Displaced. `dispatch_input` at 3920-3988 accepts generic InputReq without current-controller validation; contrast controller-exclusive resize, which uses `OutputLog::is_controller(send)` at 4209-4213. Generic input also serves non-RC system paths, so ownership validation should be additive/RC-specific rather than blindly applied to every InputReq."
    },
    {
      "path": "crates/spt-daemon/src/brain.rs",
      "description": "Broker IPC translation. `Brain::attach_as` at 982-1007 carries intent/gen/by into SubscribeReq; `read_event` decodes KIND_DISPLACED at 797-804. `detach_session_gen` at 1803-1812 sends generation-validated unsubscribe. `subscribe_with` at 1838-1855 serializes the ownership fields. Nothing here drops a correctly delivered Displaced; the event is never generated in the same-node branch."
    },
    {
      "path": "crates/spt-daemon/src/msg.rs",
      "description": "Internal controller protocol. SubscribeReq at 391-414 documents `by` as node identity and `gen` as wall-ms. SubscribeOutcome at 417-433 distinguishes Controller, BusyControlled, and TookControl. KIND_DISPLACED contract at 75-82 explicitly promises forwarding plus detach but also encodes the mistaken assumption that any same-origin successor is a restart re-serve. InputReq at 489-521 has session/data/op/minter/ack but no controller ownership token, enabling stale serve workers to inject."
    },
    {
      "path": "crates/spt-daemon/src/nethost.rs",
      "description": "Why same-machine processes collide. `ConnKind::Loopback` and ConnEntry at 322-345 define a local loopback connection's `remote_id_hex` as this node's own stable id. Thus every local `rc` process reaches `serve_attach` with the same origin. The same identity collision occurs for two remote `rc` processes on one operator node because QUIC supplies that node's same handshake identity."
    },
    {
      "path": "crates/spt-net/src/net/attach.rs",
      "description": "Public wire contract is correct: AttachIntent Viewer/Control/Take at 34-54 says Take is the only displacement path; Request at 72-108 carries intent and generation; Displaced at 122-125 requires loud full detach. The implementation violates this contract only when node identity conflates distinct viewports."
    },
    {
      "path": "crates/spt-daemon/tests/attach.rs",
      "description": "Existing coverage explains the gap. `controller_viewer_matrix_and_loud_take` at 1073-1107 tests loud Take only with DIFFERENT origins (`node-A` vs `node-B`) and passes. `same_origin_re_subscribe_does_not_displace` at 1110-1154 uses gen=0 Control for both and deliberately codifies silent same-origin replacement, but does not distinguish exact stream reconstruction from a new viewport. The full local transport seam already exists in `attach_drive_detach` at 128-190 and `local_attach_via_loopback_conn_rides_the_same_pump` at 418-460."
    },
    {
      "path": "crates/spt-daemon/src/broker.rs",
      "description": "Unit-level generation coverage at 6178-6240 is the nearest deterministic red seam: `same_identity_resubscribe_refuses_a_stale_generation` currently declares equal OR newer generation a silent successor, although the same Request reconstructed after dispatcher restart is specifically the EQUAL-generation case. The release test at 6315-6343 correctly protects a newer controller from an older worker's late detach and should remain."
    },
    {
      "path": "docs/adr/0038-broker-stream-lifecycle-truth.md",
      "description": "ADR lines 17-23 describe the same historical shape—same-identity silent `become_controller` steals output and leaves a frozen PTY—and lines 50-53 preserve legitimate same-Request reconstruction. Amendment lines 117-120 requires ownership/generation validation. The present code overgeneralizes legitimate equal-generation reconstruction into every newer same-node viewport."
    },
    {
      "path": "docs/adr/0040-bounded-stream-seat-lifecycle-one-way-terminal.md",
      "description": "Lines 40-44 and 81-88 define ConnectionBound RC streams: opener death must yield FIN/detach, with late-close generation validation. This works for process death/clean detach, but takeover does not kill the old opener connection, so ConnectionBound lifetime never fires."
    },
    {
      "path": "CONTEXT.md",
      "description": "Canonical product contract at 381-386 requires exactly one interactive controller, `--take` to send a loud Displaced notice, and full detach rather than demotion. The current same-node behavior violates all three observable parts."
    }
  ],
  "architecture": "STATE-MACHINE TIMELINE\n\n1. Original CLI O: `spt rc deployah` parses to `AttachIntent::Control` (`cli.rs:1426-1428,1719-1727`). `run_attach_inner` may consult persisted `driven_by` only for Control (`rc.rs:1136-1147`). On the first attach the slot is ordinarily free or held only by the broker's local spawn reader.\n\n2. Establish O: `establish_attach` resolves deployah locally first. Local uses the broker loopback connection; remote uses the owner node's QUIC connection. The protocol above the transport leaf is identical. `request_attach_endpoint` opens a ConnectionBound stream and sends `Request{intent:Control,gen:now_ms()}`. For local loopback, the target stream's proven origin is this node's own id; for remote, it is the operator node's handshake id. In both cases two rc processes on the same operator machine have the same `by`.\n\n3. Serve O: dispatcher identifies the peer-initiated Attach stream and calls `serve_attach`; `handle_attach_request` calls `Brain::attach_as(session, Control, gen_O, Some(origin))`. The broker resolves O as Controller, installs O's ControllerSink, and stamps `driven_by=origin`, `controlled=true`. O's target serve worker records `role=Controller`; O's operator pump renders Output and sends Input.\n\n4. Takeover CLI T: `spt rc deployah --take` maps correctly to Take and bypasses the Control-only persisted-driver refusal. It establishes a second ConnectionBound attach stream with the same `by` and normally `gen_T > gen_O`.\n\n5. Defective broker transition: `resolve_subscribe` sees `controller_by()==by` and enters `same_identity` before looking at Take. Because `gen_T` is not older, it calls `become_controller`, returns ordinary `Controller`, and never executes the loud Take arm. Broker truth now has exactly one ControllerSink: T. `become_controller` drops O's sink/writer path and installs T's, so all new PTY Output goes to T. Persisted stamps remain `driven_by=the same node`, `controlled=true`; no durable projection changes.\n\n6. Orphan O: dropping O's ControllerSink is not equivalent to detaching O's attach protocol. O's operator connection is alive, so ConnectionBound does not synthesize FIN. O's target-side attach stream subscription and `serve_attach` loop remain alive. Since no BrokerEvent::Displaced was emitted, attach.rs never forwards Displaced+FIN. O's rc pump therefore remains in raw-mode pump, but receives no PTY Output. This exactly produces the original-stays-attached/stops-rendering symptom.\n\n7. Dual input: O's serve worker has no revocation transition; its local role is still Controller. Input received on O's still-open attach stream calls generic KIND_INPUT. Broker `dispatch_input` does not check that O's brain connection/gen owns the current ControllerSink, so O still writes the PTY. T also writes. Only output/resize broker truth is singular; input authority is split.\n\n8. Later detach/FIN: if O presses ctrl-b d or its process dies, its stream FIN reaches O's serve worker, which calls `detach_session_gen(gen_O)`. `detach_if_gen` sees current gen_T and correctly does not clear T. If T detaches, matching gen_T clears the broker slot and stamps. O does not automatically regain the slot; it remains an output-orphan that can still send generic input until its own stream ends. If a takeover implementation merely FINs O without first delivering Displaced, O's rc pump classifies a post-output EOF as Severed and reconnects, so terminal reason ordering is load-bearing.\n\nWHY DIFFERENT-NODE TAKE USUALLY WORKS, AND ITS RESIDUAL RISK\n\nFor different `by`, Take reaches broker.rs:1335-1347. The broker queues KIND_DISPLACED to O's controller writer, replaces the sink, and returns TookControl. O's writer sends the terminal event; O's serve loop converts it to wire Displaced+FIN; O's pump returns final and prints the loud notice. However the enqueue is `try_send` with ignored failure. A full controller queue or wedged old writer can lose the only revocation signal; replacing the sink still does not directly close/revoke the attach stream. Thus same-node failure is deterministic identity misclassification, while different-node failure remains a backpressure-dependent force-detach weakness.\n\nDETERMINISTIC REGRESSION SEAMS\n\nA. Minimal broker test, adjacent to `same_identity_resubscribe_refuses_a_stale_generation` (`broker.rs:6178`): install A with `(by=node-A, gen=100, Control)`; submit B with `(same by, gen=200, Take)`. Required: B gets TookControl and A receives KIND_DISPLACED. Current code deterministically returns Controller and emits no Displaced. Add the companion `(same by, gen=200, Control)` assertion: BusyControlled, not silent replacement. Preserve `(same by, equal gen=100)` as silent reconstruction and older gen as Busy.\n\nB. Protocol integration, adjacent to `controller_viewer_matrix_and_loud_take` / using the existing loopback seam in `tests/attach.rs`: open two real loopback ConnectionBound attach streams from two operator Brains, serve both with the same proven origin, A Control then B Take. Assert (1) B gets control/output, (2) A decodes AttachRecord::Displaced, then its stream EOFs and A's serve thread returns `AttachServeOutcome::Displaced`, (3) broker retains one controller, (4) post-take A input is rejected/does not echo while B input does. This is the deterministic same-machine reproduction; it avoids CLI timing and exercises the actual attach/FIN pump.\n\nFIX SUGGESTION\n\n1. Make logical viewport ownership `(by, generation-or-durable-stream-token)`, never `by` alone. Equal nonzero token means the exact Request is being re-served after dispatcher/brain reconstruction and may silently retake. Older token is stale and must be Busy. A distinct newer same-node viewport must obey intent: Control→Busy; Take→loud displacement+TookControl. Keep gen=0 legacy behavior for N-1 compatibility. The minimal patch can use existing gen. Stronger correctness replaces wall-ms (two processes can collide within one millisecond) with a broker-authoritative monotonic viewport token—ideally the target broker's durable inbound stream id, which remains equal across redispatch and increases for a new viewport.\n\n2. Factor one loud-take path and invoke it for both different-node Take and same-node/distinct-viewport Take. Do not let identity comparison bypass intent.\n\n3. Make revocation authoritative, not best-effort data queued behind PTY output. Provide a reserved/priority terminal-control lane or explicit serve-worker cancellation keyed by the old viewport token. The old worker must transition Controller→Displaced immediately, stop accepting input, send Displaced to its operator, then FIN. The new controller need not wait for the old terminal to drain, but broker truth must revoke it synchronously.\n\n4. Add broker-side authorization for RC input using the controller connection plus viewport token/generation. `serve_attach` must send an ownership-bearing controller-input request; broker rejects it unless it matches the current ControllerSink. Do not indiscriminately gate all generic InputReq by `is_controller`, because `shellchan` and other system delivery paths intentionally use generic input. Resize already demonstrates the current-controller check pattern.\n\n5. Keep generation-validated detach and stamp convergence. They are correct: an old FIN must not clear the replacement. Persisted `driven_by`/`controlled` remain projections for UI/routing, not controller identity; do not attempt to solve process ownership from those node-level stamps."
}