690	        let send: SharedSend = Arc::new(Mutex::new(send));
691
692	        // Version handshake: this connection must be a brain (2.3).
693	        recv_hello(&mut recv, Role::Brain)?;
694
695	        let mut my_subs: Vec<u64> = Vec::new();
696	        let mut my_stream_subs: Vec<u64> = Vec::new();
697	        let mut my_presence_sub = false;
698
699	        loop {
700	            let env = match read_frame(&mut recv) {
701	                Ok(e) => e,
702	                // Any read failure (EOF included) ends the connection.
703	                Err(_) => break,
704	            };
705	            match env.kind.as_str() {
706	                KIND_SPAWN => match self.dispatch_spawn(env, &send) {
707	                    Ok(id) => my_subs.push(id),
708	                    Err(msg) => send_error(&send, &msg),
709	                },
710	                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {
711	                    Ok(id) => my_subs.push(id),
712	                    Err(msg) => send_error(&send, &msg),
713	                },
714	                KIND_UNSUBSCRIBE => {
715	                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {
716	                        send_error(&send, &msg);
717	                    }
718	                }
719	                KIND_INPUT => {
720	                    if let Err(msg) = self.dispatch_input(env, &send) {
721	                        send_error(&send, &msg);
722	                    }
723	                }
724	                KIND_RESIZE => {
725	                    if let Err(msg) = self.dispatch_resize(env, &send) {
726	                        send_error(&send, &msg);
727	                    }
728	                }
729	                KIND_KILL => {
730	                    if let Err(msg) = self.dispatch_kill(env) {
731	                        send_error(&send, &msg);
732	                    }
733	                }
734	                KIND_NET_STATUS => self.dispatch_net_status(&send),
735	                KIND_NET_DIAL => {
736	                    if let Err(msg) = self.dispatch_net_dial(env, &send) {
737	                        send_error(&send, &msg);
738	                    }
739	                }
740	                KIND_NET_DIAL_LOOPBACK => {
741	                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {
742	                        send_error(&send, &msg);
743	                    }
744	                }
745	                KIND_PAIR_JOIN => {
746	                    if let Err(msg) = self.dispatch_pair_join(env, &send) {
747	                        send_error(&send, &msg);
748	                    }
749	                }
750	                KIND_NET_STREAM_OPEN => {
751	                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {
752	                        send_error(&send, &msg);
753	                    }
754	                }
755	                KIND_NET_STREAM_SEND => {
756	                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {
757	                        send_error(&send, &msg);
758	                    }
759	                }
760	                KIND_NET_STREAMS => {
761	                    if let Err(msg) = self.dispatch_net_streams(&send) {
762	                        send_error(&send, &msg);
763	                    }
764	                }
765	                KIND_SESSIONS => {
766	                    // Clone the (id, endpoint, log) out from under the sessions
767	                    // lock, then read each log's cursor under the log lock alone —
768	                    // never both locks at once (the dispatch_subscribe discipline).
769	                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {
770	                        let sessions = self.sessions.lock().unwrap();
771	                        sessions
772	                            .iter()
773	                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))
774	                            .collect()
775	                    };
776	                    let reply = SessionsReply {
777	                        sessions: rows
778	                            .into_iter()
779	                            .map(|(id, endpoint, log)| SessionInfo {
780	                                session_id: id,
781	                                endpoint,
782	                                resume_seq: log.lock().unwrap().delivered_through(),
783	                            })
784	                            .collect(),
785	                    };
786	                    let frame = Envelope::new(
787	                        KIND_SESSIONS_REPLY,
788	                        serde_json::to_value(reply).expect("SessionsReply serializes"),
789	                    );
790	                    send_frame(&send, &frame);
791	                }
792	                KIND_ENDPOINT_INPUT => {
793	                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {
794	                        send_error(&send, &msg);
795	                    }
796	                }
797	                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {
798	                    Ok(id) => my_stream_subs.push(id),
799	                    Err(msg) => send_error(&send, &msg),
800	                },
801	                KIND_NET_PRESENCE_SUBSCRIBE => {
802	                    match self.dispatch_net_presence_subscribe(env, &send) {
803	                        Ok(()) => my_presence_sub = true,
804	                        Err(msg) => send_error(&send, &msg),
805	                    }
806	                }
807	                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),
808	                // An unknown kind is tolerated (forward-compat): report, don't die.
809	                other => send_error(&send, &format!("unknown command kind {other:?}")),
810	            }
811	        }
812
813	        // Brain gone: detach as subscriber but leave the sessions running.
814	        let sessions = self.sessions.lock().unwrap();
815	        for id in &my_subs {
816	            if let Some(h) = sessions.get(id) {
817	                h.log.lock().unwrap().detach_if(&send);
818	            }
819	        }
820	        drop(sessions);
821	        // Same for net streams: the streams + their rings persist (D4b).
822	        if let Some(host) = self.net.get() {
823	            for id in &my_stream_subs {
824	                host.detach_stream_sub(*id, &send);
825	            }
826	            // And presence: the liveness log + its ring persist (D4c).
827	            if my_presence_sub {
828	                host.detach_presence_sub(&send);
829	            }
830	        }
831	        Ok(())
832	    }
833
834	    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning
835	    /// connection, start its output drain and exit waiter, and reply `spawned`.
836	    /// Every hosted session gets its **own** drain + exit-waiter threads (KH
837	    /// 7.4): a hung child blocks only its own threads — shell instances ride
838	    /// exactly this isolation (one owner's hung shell binary stalls neither
839	    /// another owner's delivery nor the broker's control dispatch).
840	    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]
841	    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {
842	        let req: SpawnReq =
843	            serde_json::from_value(env.payload).map_err(|e| format!("bad spawn payload: {e}"))?;
844	        // REQ-HOST-RUN-2: spawn in the requested project cwd when carried;
845	        // `None` inherits the broker's cwd (legacy/test + W1 bringup shortcut).
846	        // Inject the manifest [env] vars (REQ-HAZARD-ENV-SUBST) on top of the
847	        // inherited env — empty for a legacy/N-1 spawn (inherit only).
848	        let session = PtySession::spawn_program_in_env(
849	            &req.program,
850	            &req.args,
851	            SurfaceSize::new(req.rows, req.cols),
852	            req.cwd.as_deref(),
853	            req.env.iter().map(|(k, v)| (k.as_str(), v.as_str())),
854	        )
855	        .map_err(|e| format!("spawn failed: {e}"))?;
856	        let session = Arc::new(session);
857	        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
858	        let pid = session.process_id();
859
860	        // The output log, with the spawning connection pre-attached as the
861	        // controller (identity `None` = LOCAL, not a remote driver) so no chunk
862	        // is missed before an explicit subscribe — and so a remote attach to a
863	        // spawned-but-undriven session takes control silently (no loud displace
864	        // of the local-spawner placeholder; REQ-RCVIEW-1).
865	        let log = Arc::new(Mutex::new(OutputLog::new(
866	            id,
867	            self.log_cap_chunks,
868	            req.endpoint.clone(),
869	            (req.rows, req.cols),
870	        )));
871	        log.lock()
872	            .unwrap()
873	            .become_controller(Arc::clone(send), None, 0);
874
875	        // Output drain → appends to the log (which live-sends to the subscriber).
876	        let log_drain = Arc::clone(&log);
877	        let drain = session
878	            .drain(move |chunk| {
879	                log_drain.lock().unwrap().append(chunk);
880	            })
881	            .map_err(|e| format!("drain failed: {e}"))?;
882
883	        // Exit waiter → one `exit` frame when the child is reaped, then the dead
884	        // session is removed from the table. Runs on its own thread; `wait`
885	        // holds the child lock but `kill` uses a split killer.
886	        let sessions_exit = Arc::clone(&self.sessions);
887	        let log_exit = Arc::clone(&log);
888	        let waiter = Arc::clone(&session);
889	        thread::spawn(move || {
890	            if let Ok(status) = waiter.wait() {
891	                let ev = ExitEvent {
892	                    session_id: id,
893	                    code: Some(status.exit_code() as i32),
894	                };
895	                let frame = Envelope::new(
896	                    KIND_EXIT,
897	                    serde_json::to_value(ev).expect("ExitEvent serializes"),
898	                );
899	                // Send to every attached sink — controller + all viewers (under
900	                // the log lock).
901	                let sinks = log_exit.lock().unwrap().all_sinks();
902	                for sink in sinks {
903	                    if let Ok(mut s) = sink.lock() {
904	                        let _ = write_frame(&mut *s, &frame);
905	                    }
906	                }
907	            }
908	            // Reap the dead session from the table.
909	            sessions_exit.lock().unwrap().remove(&id);

        let send: SharedSend = Arc::new(Mutex::new(send));

        // Version handshake: this connection must be a brain (2.3).
        recv_hello(&mut recv, Role::Brain)?;

        let mut my_subs: Vec<u64> = Vec::new();
        let mut my_stream_subs: Vec<u64> = Vec::new();
        let mut my_presence_sub = false;

        loop {
            let env = match read_frame(&mut recv) {
                Ok(e) => e,
                // Any read failure (EOF included) ends the connection.
                Err(_) => break,
            };
            match env.kind.as_str() {
                KIND_SPAWN => match self.dispatch_spawn(env, &send) {
                    Ok(id) => my_subs.push(id),
                    Err(msg) => send_error(&send, &msg),
                },
                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {
                    Ok(id) => my_subs.push(id),
                    Err(msg) => send_error(&send, &msg),
                },
                KIND_UNSUBSCRIBE => {
                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_INPUT => {
                    if let Err(msg) = self.dispatch_input(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_RESIZE => {
                    if let Err(msg) = self.dispatch_resize(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_KILL => {
                    if let Err(msg) = self.dispatch_kill(env) {
                        send_error(&send, &msg);
                    }
                }
                KIND_NET_STATUS => self.dispatch_net_status(&send),
                KIND_NET_DIAL => {
                    if let Err(msg) = self.dispatch_net_dial(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_NET_DIAL_LOOPBACK => {
                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_PAIR_JOIN => {
                    if let Err(msg) = self.dispatch_pair_join(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_NET_STREAM_OPEN => {
                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_NET_STREAM_SEND => {
                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_NET_STREAMS => {
                    if let Err(msg) = self.dispatch_net_streams(&send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_SESSIONS => {
                    // Clone the (id, endpoint, log) out from under the sessions
                    // lock, then read each log's cursor under the log lock alone —
                    // never both locks at once (the dispatch_subscribe discipline).
                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {
                        let sessions = self.sessions.lock().unwrap();
                        sessions
                            .iter()
                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))
                            .collect()
                    };
                    let reply = SessionsReply {
                        sessions: rows
                            .into_iter()
                            .map(|(id, endpoint, log)| SessionInfo {
                                session_id: id,
                                endpoint,
                                resume_seq: log.lock().unwrap().delivered_through(),
                            })
                            .collect(),
                    };
                    let frame = Envelope::new(
                        KIND_SESSIONS_REPLY,
                        serde_json::to_value(reply).expect("SessionsReply serializes"),
                    );
                    send_frame(&send, &frame);
                }
                KIND_ENDPOINT_INPUT => {
                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {
                    Ok(id) => my_stream_subs.push(id),
                    Err(msg) => send_error(&send, &msg),
                },
                KIND_NET_PRESENCE_SUBSCRIBE => {
                    match self.dispatch_net_presence_subscribe(env, &send) {
                        Ok(()) => my_presence_sub = true,
                        Err(msg) => send_error(&send, &msg),
                    }
                }
                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),
                // An unknown kind is tolerated (forward-compat): report, don't die.
                other => send_error(&send, &format!("unknown command kind {other:?}")),
            }
        }

        // Brain gone: detach as subscriber but leave the sessions running.
        let sessions = self.sessions.lock().unwrap();
        for id in &my_subs {
            if let Some(h) = sessions.get(id) {
                h.log.lock().unwrap().detach_if(&send);
            }
        }
        drop(sessions);
        // Same for net streams: the streams + their rings persist (D4b).
        if let Some(host) = self.net.get() {
            for id in &my_stream_subs {
                host.detach_stream_sub(*id, &send);
            }
            // And presence: the liveness log + its ring persist (D4c).
            if my_presence_sub {
                host.detach_presence_sub(&send);
            }
        }
        Ok(())
    }

    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning
    /// connection, start its output drain and exit waiter, and reply `spawned`.
    /// Every hosted session gets its **own** drain + exit-waiter threads (KH
    /// 7.4): a hung child blocks only its own threads — shell instances ride
    /// exactly this isolation (one owner's hung shell binary stalls neither
    /// another owner's delivery nor the broker's control dispatch).
    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]
    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {
        let req: SpawnReq =
            serde_json::from_value(env.payload).map_err(|e| format!("bad spawn payload: {e}"))?;
        // REQ-HOST-RUN-2: spawn in the requested project cwd when carried;
        // `None` inherits the broker's cwd (legacy/test + W1 bringup shortcut).
        // Inject the manifest [env] vars (REQ-HAZARD-ENV-SUBST) on top of the
        // inherited env — empty for a legacy/N-1 spawn (inherit only).
        let session = PtySession::spawn_program_in_env(
            &req.program,
            &req.args,
            SurfaceSize::new(req.rows, req.cols),
            req.cwd.as_deref(),
            req.env.iter().map(|(k, v)| (k.as_str(), v.as_str())),
        )
        .map_err(|e| format!("spawn failed: {e}"))?;
        let session = Arc::new(session);
        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
        let pid = session.process_id();

        // The output log, with the spawning connection pre-attached as the
        // controller (identity `None` = LOCAL, not a remote driver) so no chunk
        // is missed before an explicit subscribe — and so a remote attach to a
        // spawned-but-undriven session takes control silently (no loud displace
        // of the local-spawner placeholder; REQ-RCVIEW-1).
        let log = Arc::new(Mutex::new(OutputLog::new(
            id,
            self.log_cap_chunks,
            req.endpoint.clone(),
            (req.rows, req.cols),
        )));
        log.lock()
            .unwrap()
            .become_controller(Arc::clone(send), None, 0);

        // Output drain → appends to the log (which live-sends to the subscriber).
        let log_drain = Arc::clone(&log);
        let drain = session
            .drain(move |chunk| {
                log_drain.lock().unwrap().append(chunk);
            })
            .map_err(|e| format!("drain failed: {e}"))?;

        // Exit waiter → one `exit` frame when the child is reaped, then the dead
        // session is removed from the table. Runs on its own thread; `wait`
        // holds the child lock but `kill` uses a split killer.
        let sessions_exit = Arc::clone(&self.sessions);
        let log_exit = Arc::clone(&log);
        let waiter = Arc::clone(&session);
        thread::spawn(move || {
            if let Ok(status) = waiter.wait() {
                let ev = ExitEvent {
                    session_id: id,
                    code: Some(status.exit_code() as i32),
                };
                let frame = Envelope::new(
                    KIND_EXIT,
                    serde_json::to_value(ev).expect("ExitEvent serializes"),
                );
                // Send to every attached sink — controller + all viewers (under
                // the log lock).
                let sinks = log_exit.lock().unwrap().all_sinks();
                for sink in sinks {
                    if let Ok(mut s) = sink.lock() {
                        let _ = write_frame(&mut *s, &frame);
                    }
                }
            }
            // Reap the dead session from the table.
            sessions_exit.lock().unwrap().remove(&id);


Key finding @broker.rs:860-873: `dispatch_spawn` pre-attaches the **spawning connection** as controller sink. In endpoint-run that conn is the transient `cold_start` brain → drops after Spawned → cleanup detaches its sink (L813-819). OutputLog persists; drain keeps appending to ring. Later `spt rc` must REPLAY the ring + live-fan via `serve_attach`. Read serve_attach + OutputLog replay + the known-good attach.rs test.


Grep

fn serve_attach|fn become_controller|fn add_viewer|fn append\b|fn replay|fn detach_if|local_attach_via_loopback_conn_rides_the_same_pump|broker_spawns_the_pty_child_in_the_requested_cwd


M12-W1.5-PLAN.md:56:- **T1.5.4** Int test `local_attach_via_loopback_conn_rides_the_same_pump`: spawn a session,
M12-W1.5-PLAN.md:96:  `local_attach_via_loopback_conn_rides_the_same_pump` BOTH call it. Tag `[int->REQ-RC-1]`.
crates\spt-daemon\tests\attach.rs:418:fn local_attach_via_loopback_conn_rides_the_same_pump() {
crates\spt-daemon\tests\attach.rs:462:fn broker_spawns_the_pty_child_in_the_requested_cwd() {
crates\spt-daemon\src\attach.rs:106:pub fn serve_attach(
crates\spt-daemon\src\broker.rs:212:    fn append(&mut self, bytes: &[u8]) {
crates\spt-daemon\src\broker.rs:249:    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {
crates\spt-daemon\src\broker.rs:273:    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {
crates\spt-daemon\src\broker.rs:397:    fn detach_if(&mut self, sub: &SharedSend) {
crates\spt-daemon\src\nethost.rs:192:    fn append(&mut self, bytes: &[u8]) {
crates\spt-daemon\src\nethost.rs:256:    fn detach_if(&mut self, sub: &SharedSend) {
crates\spt-daemon\src\nethost.rs:294:    fn append(&mut self, kind: &str, conn_id: u64, remote_id_hex: &str) {
crates\spt-daemon\src\nethost.rs:328:    fn detach_if(&mut self, sub: &SharedSend) {
traceable-reqs.toml:488:required_stages = ["impl", "unit", "int"]  # activated M12-W1.5: impl = additive SpawnReq.cwd + PtySession::spawn_program_in (portable-pty CommandBuilder cwd) + broker dispatch_spawn honors it + harnesshost/cli thread project cwd (std::env::current_dir); unit = spawn_program_in_lands_the_child_in_the_requested_cwd (spt-term); int = broker_spawns_the_pty_child_in_the_requested_cwd (wire field → broker spawn → PTY cwd, end to end)
traceable-reqs.toml:1160:[Omitted long matching line]
crates\spt-daemon\tests\netbroker.rs:154:fn replayed_dial_op_is_deduped_across_brain_restart() {
crates\spt-store\src\sessions.rs:97:pub fn append(perch: &Path, entry: &SessionEntry) -> io::Result<bool> {


106	pub fn serve_attach(
107	    brain: &mut Brain,
108	    stream_id: u64,
109	    endpoint: &str,
110	    origin_node: &str,
111	    net_from_seq: u64,
112	) -> io::Result<AttachServeOutcome> {
113	    brain.net_stream_subscribe(stream_id, net_from_seq)?;
114	    let mut decoder = AttachDecoder::new();
115	    let mut attached = false;
116	    // The Request's intent — gates wire input (a viewer's keystrokes never reach
117	    // the PTY) independent of the broker's later role reply (no IPC-ordering
118	    // race on the input gate).
119	    let mut intent = AttachIntent::Control;
120	    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:
121	    // controller input that races ahead of the reply is BUFFERED, not dropped
122	    // (the replayed-input-after-restart case) and not leaked to the PTY before
123	    // we know we control the session (the busy-refuse case).
124	    let mut role: Option<ServeRole> = None;
125	    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();
126	    // The attached session — captured from the Request so the terminal paths can
127	    // explicitly release this worker's role (clearing driven_by / viewer_count
128	    // promptly, not only on connection drop).
129	    let mut attached_session: Option<u64> = None;
130
131	    loop {
132	        match brain.read_event()? {
133	            // ── Wire → session: requests, input, resize. ───────────────────
134	            BrokerEvent::NetStreamData {
135	                stream_id: sid,
136	                bytes,
137	                ..
138	            } if sid == stream_id => {
139	                for rec in decoder.push(&bytes) {
140	                    match rec {
141	                        AttachRecord::Request {
142	                            session_id,
143	                            from_seq,
144	                            intent: req_intent,
145	                        } => {
146	                            // Unsolicited wire inbound — the ADR-0009 gate, under
147	                            // the transport-proven origin. Viewing is gated
148	                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:
149	                            // watching reveals full session contents).
150	                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)
151	                                == AccessDecision::Deny
152	                            {
153	                                brain.net_stream_send(stream_id, &[], None, true)?;
154	                                return Ok(AttachServeOutcome::Refused);
155	                            }
156	                            intent = req_intent;
157	                            attached_session = Some(session_id);
158	                            // Role-aware subscribe (REQ-RCVIEW-1): the broker
159	                            // resolves controller / viewer / busy / take by the
160	                            // intent + this handshake-proven origin, and replies a
161	                            // `Subscribed`. driven_by / viewer_count are now
162	                            // BROKER-owned (single writer — resolves the
163	                            // clear-race), so this loop no longer stamps them.
164	                            brain.attach_as(session_id, from_seq, req_intent, Some(origin_node))?;
165	                            attached = true;
166	                        }
167	                        AttachRecord::Input { data_b64, op_id }
168	                            if attached && intent != AttachIntent::Viewer =>
169	                        {
170	                            let bytes = decode_bytes(&data_b64)
171	                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
172	                            match role {
173	                                // Confirmed controller — exactly-once at the PTY
174	                                // (the operator-minted id rides the B5 journal).
175	                                Some(ServeRole::Controller) => brain.send_effect(op_id, &bytes)?,
176	                                // Outcome not yet known — buffer; flushed on a
177	                                // controller confirmation, discarded on busy/viewer.
178	                                None => pending_input.push((op_id, bytes)),
179	                                // A viewer never reaches here (intent gate).
180	                                Some(ServeRole::Viewer) => {}
181	                            }
182	                        }
183	                        AttachRecord::Resize { rows, cols }
184	                            if attached && intent != AttachIntent::Viewer =>
185	                        {
186	                            // Controller → PTY resize (controller-EXCLUSIVE; the
187	                            // broker rejects it on a viewer's stream). A viewer's
188	                            // Resize is dropped here by the intent gate too.
189	                            let _ = brain.resize(rows, cols);
190	                        }
191	                        // Input/Resize before Request, a viewer's input/resize,
192	                        // or target-direction records echoed back: noise — ignore.
193	                        _ => {}
194	                    }
195	                }
196	            }
197
198	            // ── Broker → loop: the resolved controller/viewer role. ─────────
199	            BrokerEvent::Subscribed { outcome, .. } => {
200	                use crate::msg::SubscribeOutcome as O;
201	                match outcome {
202	                    O::Controller | O::TookControl => {
203	                        role = Some(ServeRole::Controller);
204	                        // A driver attached — the wake edge (controller-only;
205	                        // idempotent on an already-active seat).
206	                        feed_rest(endpoint, crate::resting::RestEvent::Wake);
207	                        // Flush any input that raced ahead of the reply.
208	                        for (op, b) in pending_input.drain(..) {
209	                            brain.send_effect(op, &b)?;
210	                        }
211	                    }
212	                    O::Viewer => {
213	                        role = Some(ServeRole::Viewer);
214	                        // Wake-neutral: a viewer never wakes the endpoint, never
215	                        // keeps it awake (Gap B). Discard any racing input.
216	                        pending_input.clear();
217	                    }
218	                    O::BusyControlled { .. } => {
219	                        // A different operator controls it — refuse (no PTY input
220	                        // ever reached it; the buffer is discarded). The rc
221	                        // renders the busy guidance client-side from driven_by.
222	                        brain.net_stream_send(stream_id, &[], None, true)?;
223	                        return Ok(AttachServeOutcome::Refused);
224	                    }
225	                }
226	            }
227	            // Viewer letterbox feed: the current PTY size (on attach + resize).
228	            BrokerEvent::Size { rows, cols, .. } if attached => {
229	                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });
230	                brain.net_stream_send(stream_id, &line, None, false)?;
231	            }
232	            // This controller was kicked by a different operator's `--take`.
233	            BrokerEvent::Displaced { by, .. } if attached => {
234	                let line = ndjson::encode_line(&AttachRecord::Displaced { by });
235	                brain.net_stream_send(stream_id, &line, None, true)?;
236	                // No dormant edge: the endpoint stays driven by the taker (whose
237	                // own serve loop already fired the wake).
238	                return Ok(AttachServeOutcome::Displaced);
239	            }
240
241	            // The operator closed its send side: the viewport is over.
242	            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {
243	                brain.net_stream_send(stream_id, &[], None, true)?;
244	                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes
245	                // info.json, carrying the current driven_by through), THEN release
246	                // the role over IPC so the broker's driven_by=None is the LAST
247	                // write — otherwise a concurrent resting RMW restores the stale
248	                // marker (the clear-race the broker-single-writer model averts).
249	                if role == Some(ServeRole::Controller) {
250	                    // The driver detached — the endpoint rests warm (controller-
251	                    // only; a viewer leaving is wake-neutral).
252	                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
253	                }
254	                if let Some(sid) = attached_session {
255	                    let _ = brain.detach_session(sid); // release the role (clears markers)
256	                }
257	                return Ok(AttachServeOutcome::Detached);
258	            }
259
260	            // ── Session → wire: output and exit. ────────────────────────────
261	            BrokerEvent::Output { seq, bytes, .. } if attached => {
262	                let line = ndjson::encode_line(&AttachRecord::Output {
263	                    seq,
264	                    data_b64: encode_bytes(&bytes),
265	                });

pub fn serve_attach(
    brain: &mut Brain,
    stream_id: u64,
    endpoint: &str,
    origin_node: &str,
    net_from_seq: u64,
) -> io::Result<AttachServeOutcome> {
    brain.net_stream_subscribe(stream_id, net_from_seq)?;
    let mut decoder = AttachDecoder::new();
    let mut attached = false;
    // The Request's intent — gates wire input (a viewer's keystrokes never reach
    // the PTY) independent of the broker's later role reply (no IPC-ordering
    // race on the input gate).
    let mut intent = AttachIntent::Control;
    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:
    // controller input that races ahead of the reply is BUFFERED, not dropped
    // (the replayed-input-after-restart case) and not leaked to the PTY before
    // we know we control the session (the busy-refuse case).
    let mut role: Option<ServeRole> = None;
    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();
    // The attached session — captured from the Request so the terminal paths can
    // explicitly release this worker's role (clearing driven_by / viewer_count
    // promptly, not only on connection drop).
    let mut attached_session: Option<u64> = None;

    loop {
        match brain.read_event()? {
            // ── Wire → session: requests, input, resize. ───────────────────
            BrokerEvent::NetStreamData {
                stream_id: sid,
                bytes,
                ..
            } if sid == stream_id => {
                for rec in decoder.push(&bytes) {
                    match rec {
                        AttachRecord::Request {
                            session_id,
                            from_seq,
                            intent: req_intent,
                        } => {
                            // Unsolicited wire inbound — the ADR-0009 gate, under
                            // the transport-proven origin. Viewing is gated
                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:
                            // watching reveals full session contents).
                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)
                                == AccessDecision::Deny
                            {
                                brain.net_stream_send(stream_id, &[], None, true)?;
                                return Ok(AttachServeOutcome::Refused);
                            }
                            intent = req_intent;
                            attached_session = Some(session_id);
                            // Role-aware subscribe (REQ-RCVIEW-1): the broker
                            // resolves controller / viewer / busy / take by the
                            // intent + this handshake-proven origin, and replies a
                            // `Subscribed`. driven_by / viewer_count are now
                            // BROKER-owned (single writer — resolves the
                            // clear-race), so this loop no longer stamps them.
                            brain.attach_as(session_id, from_seq, req_intent, Some(origin_node))?;
                            attached = true;
                        }
                        AttachRecord::Input { data_b64, op_id }
                            if attached && intent != AttachIntent::Viewer =>
                        {
                            let bytes = decode_bytes(&data_b64)
                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                            match role {
                                // Confirmed controller — exactly-once at the PTY
                                // (the operator-minted id rides the B5 journal).
                                Some(ServeRole::Controller) => brain.send_effect(op_id, &bytes)?,
                                // Outcome not yet known — buffer; flushed on a
                                // controller confirmation, discarded on busy/viewer.
                                None => pending_input.push((op_id, bytes)),
                                // A viewer never reaches here (intent gate).
                                Some(ServeRole::Viewer) => {}
                            }
                        }
                        AttachRecord::Resize { rows, cols }
                            if attached && intent != AttachIntent::Viewer =>
                        {
                            // Controller → PTY resize (controller-EXCLUSIVE; the
                            // broker rejects it on a viewer's stream). A viewer's
                            // Resize is dropped here by the intent gate too.
                            let _ = brain.resize(rows, cols);
                        }
                        // Input/Resize before Request, a viewer's input/resize,
                        // or target-direction records echoed back: noise — ignore.
                        _ => {}
                    }
                }
            }

            // ── Broker → loop: the resolved controller/viewer role. ─────────
            BrokerEvent::Subscribed { outcome, .. } => {
                use crate::msg::SubscribeOutcome as O;
                match outcome {
                    O::Controller | O::TookControl => {
                        role = Some(ServeRole::Controller);
                        // A driver attached — the wake edge (controller-only;
                        // idempotent on an already-active seat).
                        feed_rest(endpoint, crate::resting::RestEvent::Wake);
                        // Flush any input that raced ahead of the reply.
                        for (op, b) in pending_input.drain(..) {
                            brain.send_effect(op, &b)?;
                        }
                    }
                    O::Viewer => {
                        role = Some(ServeRole::Viewer);
                        // Wake-neutral: a viewer never wakes the endpoint, never
                        // keeps it awake (Gap B). Discard any racing input.
                        pending_input.clear();
                    }
                    O::BusyControlled { .. } => {
                        // A different operator controls it — refuse (no PTY input
                        // ever reached it; the buffer is discarded). The rc
                        // renders the busy guidance client-side from driven_by.
                        brain.net_stream_send(stream_id, &[], None, true)?;
                        return Ok(AttachServeOutcome::Refused);
                    }
                }
            }
            // Viewer letterbox feed: the current PTY size (on attach + resize).
            BrokerEvent::Size { rows, cols, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });
                brain.net_stream_send(stream_id, &line, None, false)?;
            }
            // This controller was kicked by a different operator's `--take`.
            BrokerEvent::Displaced { by, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Displaced { by });
                brain.net_stream_send(stream_id, &line, None, true)?;
                // No dormant edge: the endpoint stays driven by the taker (whose
                // own serve loop already fired the wake).
                return Ok(AttachServeOutcome::Displaced);
            }

            // The operator closed its send side: the viewport is over.
            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {
                brain.net_stream_send(stream_id, &[], None, true)?;
                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes
                // info.json, carrying the current driven_by through), THEN release
                // the role over IPC so the broker's driven_by=None is the LAST
                // write — otherwise a concurrent resting RMW restores the stale
                // marker (the clear-race the broker-single-writer model averts).
                if role == Some(ServeRole::Controller) {
                    // The driver detached — the endpoint rests warm (controller-
                    // only; a viewer leaving is wake-neutral).
                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
                }
                if let Some(sid) = attached_session {
                    let _ = brain.detach_session(sid); // release the role (clears markers)
                }
                return Ok(AttachServeOutcome::Detached);
            }

            // ── Session → wire: output and exit. ────────────────────────────
            BrokerEvent::Output { seq, bytes, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Output {
                    seq,
                    data_b64: encode_bytes(&bytes),
                });


serve_attach calls `brain.attach_as(session_id, from_seq=0, ...)` → broker replays from seq 0 + live-fans via BrokerEvent::Output. Read OutputLog: append/become_controller/add_viewer/detach_if (broker.rs 200-410) to see replay + live-send mechanics.


150	    /// Last-known PTY size — seeded at spawn, updated on a controller resize.
151	    /// Pushed to a viewer on attach + to all viewers on resize (the letterbox
152	    /// feed); the broker holds it since the PTY exposes no size getter.
153	    size: (u16, u16),
154	    /// Cursor-of-record (restoration D4-1, ADR-0018 Q6): the next seq a
155	    /// re-attaching CONTROLLER still needs (= highest seq **successfully written**
156	    /// to the controller, +1). It is **log** state, not subscriber state, so it
157	    /// **survives `detach_if`** — a dead brain leaves its resume point behind, and
158	    /// a cold-starting brain reads it (via `KIND_SESSIONS` → `resume_seq`) instead
159	    /// of replaying the whole ring from 0. **Viewer writes NEVER move it**
160	    /// (REQ-RCVIEW-1: the brain-resume cursor tracks the controller only).
161	    ///
162	    /// **Advanced only on a successful socket write** (`Ok`), and **monotonic**
163	    /// (never decreases) — see [`advanced_cursor`]. Both invariants are
164	    /// load-bearing for at-least-once output: advancing past a *failed* write to a
165	    /// dying-but-not-yet-detached controller would make resume skip that chunk
166	    /// forever (at-least-once → at-most-once); decreasing on a deliberate
167	    /// `attach(sub, 0)` rewind-replay would reset the resume point under a live
168	    /// reader.
169	    delivered_through: u64,
170	}
171
172	/// The post-write resume cursor (pure — the D4-1 unit kernel, mirroring
173	/// `brainproc::consumer_gate`/`next_backoff`): advance to `seq + 1` **only** on a
174	/// successful write, and **monotonically** (never below `current`). A failed
175	/// write (`write_ok == false`) leaves the cursor untouched, and a re-send of an
176	/// already-delivered `seq` (an `attach(sub, 0)` rewind, `seq + 1 <= current`)
177	/// cannot lower it. This is the exact decision the `let _ = write_frame(...)`
178	/// swallow used to drop (doyle D4-1 amendments 1–3).
179	// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]
180	fn advanced_cursor(current: u64, seq: u64, write_ok: bool) -> u64 {
181	    if write_ok {
182	        current.max(seq + 1)
183	    } else {
184	        current
185	    }
186	}
187
188	impl OutputLog {
189	    fn new(session_id: u64, cap_chunks: usize, endpoint: String, size: (u16, u16)) -> Self {
190	        OutputLog {
191	            session_id,
192	            next_seq: 0,
193	            ring: VecDeque::new(),
194	            cap_chunks,
195	            controller: None,
196	            controller_by: None,
197	            viewers: HashMap::new(),
198	            next_viewer_id: 0,
199	            endpoint,
200	            size,
201	            delivered_through: 0,
202	        }
203	    }
204
205	    /// Append a freshly-produced output chunk: assign the next `seq`, retain it
206	    /// in the bounded ring, then FAN OUT — the controller on the authoritative
207	    /// blocking path (advancing the delivered cursor only on a successful write,
208	    /// D4-1), and each viewer via a `try_send` to its bounded queue, EVICTING any
209	    /// viewer whose queue overflows or whose writer has gone. The drain thread
210	    /// NEVER touches a viewer socket, so no viewer can stall it
211	    /// (REQ-HAZARD-VIEWER-ISOLATION). Called only from the session's drain thread.
212	    fn append(&mut self, bytes: &[u8]) {
213	        let seq = self.next_seq;
214	        self.next_seq += 1;
215	        self.ring.push_back((seq, bytes.to_vec()));
216	        while self.ring.len() > self.cap_chunks {
217	            self.ring.pop_front();
218	        }
219	        let frame = output_envelope(self.session_id, seq, bytes);
220	        // Controller: authoritative, blocking, advances the resume cursor.
221	        if let Some(sub) = &self.controller {
222	            let write_ok = match sub.lock() {
223	                Ok(mut s) => write_frame(&mut *s, &frame).is_ok(),
224	                Err(_) => false,
225	            };
226	            self.delivered_through = advanced_cursor(self.delivered_through, seq, write_ok);
227	        }
228	        // Viewers: non-blocking fan-out; a full / disconnected queue = eviction.
229	        if !self.viewers.is_empty() {
230	            let mut evicted: Vec<u64> = Vec::new();
231	            for (vid, sink) in self.viewers.iter() {
232	                if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {
233	                    evicted.push(*vid);
234	                }
235	            }
236	            if !evicted.is_empty() {
237	                for vid in evicted {
238	                    self.viewers.remove(&vid);
239	                }
240	                self.stamp_viewer_count();
241	            }
242	        }
243	    }
244
245	    /// Become (or silently re-take) the interactive controller from `from_seq`.
246	    /// Replays the ring to the new controller and advances the delivered cursor
247	    /// to the last-successfully-written seq + 1 (D4-1: never the unsent tail, and
248	    /// only ever forward). Stamps `driven_by` to the new controller's identity.
249	    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {
250	        self.controller = Some(Arc::clone(&sub));
251	        self.controller_by = by;
252	        let mut last_ok: Option<u64> = None;
253	        if let Ok(mut s) = sub.lock() {
254	            for (seq, bytes) in self.ring.iter().filter(|(s, _)| *s >= from_seq) {
255	                let frame = output_envelope(self.session_id, *seq, bytes);
256	                if write_frame(&mut *s, &frame).is_ok() {
257	                    last_ok = Some(*seq);
258	                } else {
259	                    break; // stop at the first failed write; cursor stays put
260	                }
261	            }
262	        }
263	        if let Some(seq) = last_ok {
264	            self.delivered_through = advanced_cursor(self.delivered_through, seq, true);
265	        }
266	        self.stamp_driven_by();
267	    }
268
269	    /// Add a read-only viewer: snapshot the ring from `from_seq` into the writer
270	    /// thread (NOT through the bounded live queue — replay must not trip the
271	    /// overflow eviction), seed it the current size frame first, then run the
272	    /// live queue. The drain thread fans live frames in via `try_send`.
273	    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {
274	        let vid = self.next_viewer_id;
275	        self.next_viewer_id += 1;
276	        // The size frame + the ring replay are the writer's initial batch (owned
277	        // by the thread, not the bounded queue).
278	        let mut initial: Vec<Envelope> =
279	            vec![size_envelope(self.session_id, self.size.0, self.size.1)];
280	        initial.extend(
281	            self.ring
282	                .iter()
283	                .filter(|(s, _)| *s >= from_seq)
284	                .map(|(seq, bytes)| output_envelope(self.session_id, *seq, bytes)),
285	        );
286	        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);
287	        let writer_send = Arc::clone(&sub);
288	        let writer = thread::spawn(move || viewer_writer(writer_send, initial, rx));
289	        self.viewers.insert(
290	            vid,
291	            ViewerSink {
292	                tx,
293	                send: sub,
294	                _writer: writer,
295	            },
296	        );
297	    }
298
299	    /// Resolve a controller/viewer subscribe under the W2.5 model
300	    /// (REQ-RCVIEW-1 / REQ-KICK-1). Returns the [`SubscribeOutcome`]; performs
301	    /// the controller take / viewer add / loud displace as a side effect. See the
302	    /// identity matrix in the body — the `by`-keyed logic is what makes a
303	    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.
304	    fn resolve_subscribe(
305	        &mut self,
306	        sub: SharedSend,
307	        from_seq: u64,
308	        intent: AttachIntent,
309	        by: Option<String>,
310	    ) -> SubscribeOutcome {
311	        if intent == AttachIntent::Viewer {
312	            if self.viewers.len() >= MAX_VIEWERS {
313	                // Soft cap reached — refuse the viewer (bounds thread count).
314	                return SubscribeOutcome::BusyControlled {
315	                    by: "viewer cap reached".to_string(),
316	                };
317	            }
318	            self.add_viewer(sub, from_seq);
319	            self.stamp_viewer_count();
320	            return SubscribeOutcome::Viewer;
321	        }
322	        // Control or Take.
323	        if self.controller.is_none() {
324	            // Free endpoint → become controller (nothing to displace).
325	            self.become_controller(sub, by, from_seq);
326	            return SubscribeOutcome::Controller;
327	        }
328	        let same_identity = self.controller_by == by;
329	        if same_identity {
330	            // Same operator (or both local) re-serving — silent re-take. This is
331	            // the gate-#7 path: a successor re-taking the slot after a brain
332	            // restart NEVER fires Displaced.
333	            self.become_controller(sub, by, from_seq);
334	            return SubscribeOutcome::Controller;
335	        }
336	        if by.is_none() {
337	            // The LOCAL brain re-attaching while a REMOTE controls — never steal;
338	            // fall back to a silent viewer (gate #7 for the local-resume path).
339	            self.add_viewer(sub, from_seq);
340	            self.stamp_viewer_count();
341	            return SubscribeOutcome::Viewer;
342	        }
343	        if self.controller_by.is_none() {
344	            // A remote takes an UNDRIVEN session (only the local spawner held the
345	            // slot) — silent control, no loud displace (there is no remote driver
346	            // to notify).
347	            self.become_controller(sub, by, from_seq);
348	            return SubscribeOutcome::Controller;
349	        }
350	        // A DIFFERENT remote controls it.
351	        match intent {
352	            AttachIntent::Take => {
353	                // The ONLY loud-displace path: notify the incumbent, then take.
354	                if let Some(old) = &self.controller {
355	                    let frame = displaced_envelope(self.session_id, by.as_deref().unwrap_or(""));
356	                    if let Ok(mut s) = old.lock() {
357	                        let _ = write_frame(&mut *s, &frame);
358	                    }
359	                }
360	                self.become_controller(sub, by, from_seq);
361	                SubscribeOutcome::TookControl
362	            }
363	            _ => SubscribeOutcome::BusyControlled {
364	                by: self.controller_by.clone().unwrap_or_default(),
365	            },
366	        }
367	    }
368
369	    /// Update the stored PTY size and push a `size` frame to every viewer (the
370	    /// letterbox feed). Called on a controller resize. Viewer pushes are
371	    /// non-blocking with eviction, like [`OutputLog::append`].
372	    fn set_size_and_notify(&mut self, rows: u16, cols: u16) {
373	        self.size = (rows, cols);
374	        if self.viewers.is_empty() {
375	            return;
376	        }
377	        let frame = size_envelope(self.session_id, rows, cols);
378	        let mut evicted: Vec<u64> = Vec::new();
379	        for (vid, sink) in self.viewers.iter() {
380	            if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {
381	                evicted.push(*vid);
382	            }
383	        }
384	        if !evicted.is_empty() {
385	            for vid in evicted {
386	                self.viewers.remove(&vid);
387	            }
388	            self.stamp_viewer_count();
389	        }
390	    }
391
392	    /// Detach `sub` from whatever role it held: clear the controller slot iff it
393	    /// is the current controller (a no-op if a newer brain took over — a late
394	    /// disconnect can't unseat the live controller), and remove any viewer rows
395	    /// owned by `sub`. Leaves `delivered_through` intact (D4-1). Re-stamps
396	    /// `driven_by` / `viewer_count` when a role actually changed.
397	    fn detach_if(&mut self, sub: &SharedSend) {
398	        if let Some(cur) = &self.controller {
399	            if Arc::ptr_eq(cur, sub) {
400	                self.controller = None;
401	                self.controller_by = None;
402	                self.stamp_driven_by();
403	            }
404	        }
405	        let before = self.viewers.len();
406	        self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));
407	        if self.viewers.len() != before {
408	            self.stamp_viewer_count();
409	        }
410	    }
411
412	    /// Every sink currently attached (controller + viewers) — the recipients of
413	    /// an `exit` frame.
414	    fn all_sinks(&self) -> Vec<SharedSend> {
415	        let mut v: Vec<SharedSend> = Vec::new();
416	        if let Some(c) = &self.controller {
417	            v.push(Arc::clone(c));
418	        }
419	        for sink in self.viewers.values() {
420	            v.push(Arc::clone(&sink.send));
421	        }
422	        v
423	    }
424
425	    /// Stamp the perch's `driven_by` to the current controller's identity (the
426	    /// broker is the single writer — resolves the clear-race). The remote-drive
427	    /// detection fact (REQ-REACH-1) moved here from `serve_attach` so a displaced
428	    /// operator's teardown can never wipe the new controller's marker.
429	    /// Best-effort: a session served without a perch (raw broker tests) has
430	    /// nowhere to record.
431	    // [impl->REQ-RCVIEW-1] [impl->REQ-REACH-1]
432	    fn stamp_driven_by(&self) {
433	        if self.endpoint.is_empty() {
434	            return;
435	        }
436	        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);
437	        let _ = spt_store::info::set_driven_by(&perch, self.controller_by.as_deref());
438	    }
439
440	    /// Stamp the perch's `viewer_count` to the live viewer count (broker is the
441	    /// single writer). Best-effort, like [`OutputLog::stamp_driven_by`].
442	    // [impl->REQ-RCVIEW-1]
443	    fn stamp_viewer_count(&self) {
444	        if self.endpoint.is_empty() {
445	            return;
446	        }
447	        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);
448	        let _ = spt_store::info::set_viewer_count(&perch, self.viewers.len() as u32);
449	    }
450
451	    /// Whether `sub` is the current controller (the resize-exclusivity check).
452	    fn is_controller(&self, sub: &SharedSend) -> bool {
453	        self.controller
454	            .as_ref()
455	            .is_some_and(|c| Arc::ptr_eq(c, sub))
456	    }
457
458	    /// The next seq a re-attaching controller still needs (the persisted resume
459	    /// cursor surfaced in `KIND_SESSIONS` → `resume_seq`).
460	    fn delivered_through(&self) -> u64 {
461	        self.delivered_through
462	    }
463	}
464
465	/// A viewer's dedicated writer thread (REQ-HAZARD-VIEWER-ISOLATION): write the
466	/// `initial` batch (size frame + ring replay) once, then drain live frames from
467	/// `rx` with a BLOCKING socket write each. A failed write (dead viewer) ends the
468	/// thread; the drain thread's next `try_send` then sees `Disconnected` and
469	/// evicts the row. The drain thread itself never blocks on this socket.
470	fn viewer_writer(send: SharedSend, initial: Vec<Envelope>, rx: Receiver<Envelope>) {
471	    for frame in &initial {
472	        let ok = match send.lock() {
473	            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),
474	            Err(_) => false,
475	        };
476	        if !ok {
477	            return;
478	        }
479	    }
480	    while let Ok(frame) = rx.recv() {
481	        let ok = match send.lock() {
482	            Ok(mut s) => write_frame(&mut *s, &frame).is_ok(),
483	            Err(_) => false,
484	        };
485	        if !ok {
486	            return;
487	        }
488	    }
489	}
490
491	/// A child the broker hosts under a PTY — the §B-owned resources for one
492	/// session, plus its gapless-resume output log.
493	struct HostedSession {
494	    /// The PTY + child (PTY master/writer + spawned child process).
495	    session: Arc<PtySession>,
496	    /// The output drain pump (kept so teardown can stop it).
497	    #[allow(dead_code)] // held for ownership/teardown; not read directly
498	    drain: Drain,
499	    /// The sequenced, buffered output + current subscriber.
500	    log: Arc<Mutex<OutputLog>>,
501	    /// The owning endpoint (the spawn's label; empty = unlabeled). The
502	    /// inbound dispatcher's gate subject for remote-drive (D9-1).
503	    endpoint: String,
504	}
505
506	/// The broker process kernel. Owns the listening socket, the table of hosted
507	/// sessions, and the broker-owned idempotency anchor (B5); holds no logic state
508	/// (ADR-0004 §B). The [`EffectJournal`] is a §B resource — the durable
509	/// recovery anchor a brain crash must not lose, so it lives in the broker, not
510	/// the brain (Spike #6 constraint 1).
511	pub struct Broker {
512	    listener: LocalSocketTransport,
513	    sessions: Arc<Mutex<HashMap<u64, HostedSession>>>,
514	    next_id: AtomicU64,
515	    log_cap_chunks: usize,
516	    /// The exactly-once anchor for idempotent (`op_id`-carrying) effects.
517	    journal: Arc<EffectJournal>,
518	    /// The broker-owned net endpoint + live QUIC connections (D4a, ADR-0004 §B).
519	    /// Empty when bound without a network host (net-disabled / most tests) OR
520	    /// when net is still being brought up (REQ-DAEMON-9 boot-race self-heal):
521	    /// a `OnceLock` so the background net-retry can ATTACH the host after bind
522	    /// without a relock on the hot effect-dispatch read path (`.get()` is
523	    /// lock-free). Settable exactly once — the first host wins.
524	    net: std::sync::OnceLock<NetHost>,
525	    /// The broker→supervisor planned-restart signal (ADR-0018 D3-3): a
526	    /// `KIND_BRAIN_RESTART` handler raises it so the brain supervisor cycles the
527	    /// child onto a freshly-swapped binary. Empty on a bare broker (a test broker
528	    /// with no supervisor) — the verb is then accepted but unhonored. Set once by
529	    /// `Daemon::run` after it spawns the supervisor.
530	    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,
531	}
532
533	impl Broker {
534	    /// Bind the broker's listening endpoint at the namespaced `name`, with the
535	    /// effect journal at the default path (`<spt_home>/daemon-effects.log`).
536	    pub fn bind(name: &str) -> io::Result<Arc<Self>> {
537	        Self::bind_in(name, default_journal_path())
538	    }
539
540	    /// Bind with an explicit effect-journal path — the testable seam (tests point
541	    /// the broker-owned anchor at a temp file rather than the shared home).
542	    pub fn bind_in(name: &str, journal_path: std::path::PathBuf) -> io::Result<Arc<Self>> {
543	        Self::bind_in_with_net(name, journal_path, None)
544	    }
545
546	    /// Bind with an already-started broker-owned network host (D4a). Taking the
547	    /// host (not its config) keeps a net bind failure separable from a listener
548	    /// bind failure — the daemon degrades to a net-less broker rather than
549	    /// losing PTY hosting (see `Daemon::run`).
550	    // [impl->REQ-DAEMON-2]
551	    pub fn bind_in_with_net(
552	        name: &str,
553	        journal_path: std::path::PathBuf,
554	        net: Option<NetHost>,
555	    ) -> io::Result<Arc<Self>> {
556	        Self::bind_in_with_net_cap(name, journal_path, net, DEFAULT_LOG_CHUNKS)
557	    }
558
559	    /// [`bind_in_with_net`](Self::bind_in_with_net) with an explicit output-log
560	    /// ring cap (the per-session eviction bound). Production binds at
561	    /// [`DEFAULT_LOG_CHUNKS`]; the restoration D4-2 resume harness binds a **tiny**
562	    /// cap to force ring eviction and exercise the post-eviction floor clamp (a
563	    /// `resume_seq` below the surviving ring floor must resume forward, losing only
564	    /// the evicted tail — never gap-erroring the live stream).
565	    pub fn bind_in_with_net_cap(
566	        name: &str,
567	        journal_path: std::path::PathBuf,
568	        net: Option<NetHost>,
569	        log_cap_chunks: usize,

    /// Last-known PTY size — seeded at spawn, updated on a controller resize.
    /// Pushed to a viewer on attach + to all viewers on resize (the letterbox
    /// feed); the broker holds it since the PTY exposes no size getter.
    size: (u16, u16),
    /// Cursor-of-record (restoration D4-1, ADR-0018 Q6): the next seq a
    /// re-attaching CONTROLLER still needs (= highest seq **successfully written**
    /// to the controller, +1). It is **log** state, not subscriber state, so it
    /// **survives `detach_if`** — a dead brain leaves its resume point behind, and
    /// a cold-starting brain reads it (via `KIND_SESSIONS` → `resume_seq`) instead
    /// of replaying the whole ring from 0. **Viewer writes NEVER move it**
    /// (REQ-RCVIEW-1: the brain-resume cursor tracks the controller only).
    ///
    /// **Advanced only on a successful socket write** (`Ok`), and **monotonic**
    /// (never decreases) — see [`advanced_cursor`]. Both invariants are
    /// load-bearing for at-least-once output: advancing past a *failed* write to a
    /// dying-but-not-yet-detached controller would make resume skip that chunk
    /// forever (at-least-once → at-most-once); decreasing on a deliberate
    /// `attach(sub, 0)` rewind-replay would reset the resume point under a live
    /// reader.
    delivered_through: u64,
}

/// The post-write resume cursor (pure — the D4-1 unit kernel, mirroring
/// `brainproc::consumer_gate`/`next_backoff`): advance to `seq + 1` **only** on a
/// successful write, and **monotonically** (never below `current`). A failed
/// write (`write_ok == false`) leaves the cursor untouched, and a re-send of an
/// already-delivered `seq` (an `attach(sub, 0)` rewind, `seq + 1 <= current`)
/// cannot lower it. This is the exact decision the `let _ = write_frame(...)`
/// swallow used to drop (doyle D4-1 amendments 1–3).
// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]
fn advanced_cursor(current: u64, seq: u64, write_ok: bool) -> u64 {
    if write_ok {
        current.max(seq + 1)
    } else {
        current
    }
}

impl OutputLog {
    fn new(session_id: u64, cap_chunks: usize, endpoint: String, size: (u16, u16)) -> Self {
        OutputLog {
            session_id,
            next_seq: 0,
            ring: VecDeque::new(),
            cap_chunks,
            controller: None,
            controller_by: None,
            viewers: HashMap::new(),
            next_viewer_id: 0,
            endpoint,
            size,
            delivered_through: 0,
        }
    }

    /// Append a freshly-produced output chunk: assign the next `seq`, retain it
    /// in the bounded ring, then FAN OUT — the controller on the authoritative
    /// blocking path (advancing the delivered cursor only on a successful write,
    /// D4-1), and each viewer via a `try_send` to its bounded queue, EVICTING any
    /// viewer whose queue overflows or whose writer has gone. The drain thread
    /// NEVER touches a viewer socket, so no viewer can stall it
    /// (REQ-HAZARD-VIEWER-ISOLATION). Called only from the session's drain thread.
    fn append(&mut self, bytes: &[u8]) {
        let seq = self.next_seq;
        self.next_seq += 1;
        self.ring.push_back((seq, bytes.to_vec()));
        while self.ring.len() > self.cap_chunks {
            self.ring.pop_front();
        }
        let frame = output_envelope(self.session_id, seq, bytes);
        // Controller: authoritative, blocking, advances the resume cursor.
        if let Some(sub) = &self.controller {
            let write_ok = match sub.lock() {
                Ok(mut s) => write_frame(&mut *s, &frame).is_ok(),
                Err(_) => false,
            };
            self.delivered_through = advanced_cursor(self.delivered_through, seq, write_ok);
        }
        // Viewers: non-blocking fan-out; a full / disconnected queue = eviction.
        if !self.viewers.is_empty() {
            let mut evicted: Vec<u64> = Vec::new();
            for (vid, sink) in self.viewers.iter() {
                if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {
                    evicted.push(*vid);
                }
            }
            if !evicted.is_empty() {
                for vid in evicted {
                    self.viewers.remove(&vid);
                }
                self.stamp_viewer_count();
            }
        }
    }

    /// Become (or silently re-take) the interactive controller from `from_seq`.
    /// Replays the ring to the new controller and advances the delivered cursor
    /// to the last-successfully-written seq + 1 (D4-1: never the unsent tail, and
    /// only ever forward). Stamps `driven_by` to the new controller's identity.
    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {
        self.controller = Some(Arc::clone(&sub));
        self.controller_by = by;
        let mut last_ok: Option<u64> = None;
        if let Ok(mut s) = sub.lock() {
            for (seq, bytes) in self.ring.iter().filter(|(s, _)| *s >= from_seq) {
                let frame = output_envelope(self.session_id, *seq, bytes);
                if write_frame(&mut *s, &frame).is_ok() {
                    last_ok = Some(*seq);
                } else {
                    break; // stop at the first failed write; cursor stays put
                }
            }
        }
        if let Some(seq) = last_ok {
            self.delivered_through = advanced_cursor(self.delivered_through, seq, true);
        }
        self.stamp_driven_by();
    }

    /// Add a read-only viewer: snapshot the ring from `from_seq` into the writer
    /// thread (NOT through the bounded live queue — replay must not trip the
    /// overflow eviction), seed it the current size frame first, then run the
    /// live queue. The drain thread fans live frames in via `try_send`.
    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {
        let vid = self.next_viewer_id;
        self.next_viewer_id += 1;
        // The size frame + the ring replay are the writer's initial batch (owned
        // by the thread, not the bounded queue).
        let mut initial: Vec<Envelope> =
            vec![size_envelope(self.session_id, self.size.0, self.size.1)];
        initial.extend(
            self.ring
                .iter()
                .filter(|(s, _)| *s >= from_seq)
                .map(|(seq, bytes)| output_envelope(self.session_id, *seq, bytes)),
        );
        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);
        let writer_send = Arc::clone(&sub);
        let writer = thread::spawn(move || viewer_writer(writer_send, initial, rx));
        self.viewers.insert(
            vid,
            ViewerSink {
                tx,
                send: sub,
                _writer: writer,
            },
        );
    }

    /// Resolve a controller/viewer subscribe under the W2.5 model
    /// (REQ-RCVIEW-1 / REQ-KICK-1). Returns the [`SubscribeOutcome`]; performs
    /// the controller take / viewer add / loud displace as a side effect. See the
    /// identity matrix in the body — the `by`-keyed logic is what makes a
    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.
    fn resolve_subscribe(
        &mut self,
        sub: SharedSend,
        from_seq: u64,
        intent: AttachIntent,
        by: Option<String>,
    ) -> SubscribeOutcome {
        if intent == AttachIntent::Viewer {
            if self.viewers.len() >= MAX_VIEWERS {
                // Soft cap reached — refuse the viewer (bounds thread count).
                return SubscribeOutcome::BusyControlled {
                    by: "viewer cap reached".to_string(),
                };
            }
            self.add_viewer(sub, from_seq);
            self.stamp_viewer_count();
            return SubscribeOutcome::Viewer;
        }
        // Control or Take.
        if self.controller.is_none() {
            // Free endpoint → become controller (nothing to displace).
            self.become_controller(sub, by, from_seq);
            return SubscribeOutcome::Controller;
        }
        let same_identity = self.controller_by == by;
        if same_identity {
            // Same operator (or both local) re-serving — silent re-take. This is
            // the gate-#7 path: a successor re-taking the slot after a brain
            // restart NEVER fires Displaced.
            self.become_controller(sub, by, from_seq);
            return SubscribeOutcome::Controller;
        }
        if by.is_none() {
            // The LOCAL brain re-attaching while a REMOTE controls — never steal;
            // fall back to a silent viewer (gate #7 for the local-resume path).
            self.add_viewer(sub, from_seq);
            self.stamp_viewer_count();
            return SubscribeOutcome::Viewer;
        }
        if self.controller_by.is_none() {
            // A remote takes an UNDRIVEN session (only the local spawner held the
            // slot) — silent control, no loud displace (there is no remote driver
            // to notify).
            self.become_controller(sub, by, from_seq);
            return SubscribeOutcome::Controller;
        }
        // A DIFFERENT remote controls it.
        match intent {
            AttachIntent::Take => {
                // The ONLY loud-displace path: notify the incumbent, then take.
                if let Some(old) = &self.controller {
                    let frame = displaced_envelope(self.session_id, by.as_deref().unwrap_or(""));
                    if let Ok(mut s) = old.lock() {
                        let _ = write_frame(&mut *s, &frame);
                    }
                }
                self.become_controller(sub, by, from_seq);
                SubscribeOutcome::TookControl
            }
            _ => SubscribeOutcome::BusyControlled {
                by: self.controller_by.clone().unwrap_or_default(),
            },
        }
    }

    /// Update the stored PTY size and push a `size` frame to every viewer (the
    /// letterbox feed). Called on a controller resize. Viewer pushes are
    /// non-blocking with eviction, like [`OutputLog::append`].
    fn set_size_and_notify(&mut self, rows: u16, cols: u16) {
        self.size = (rows, cols);
        if self.viewers.is_empty() {
            return;
        }
        let frame = size_envelope(self.session_id, rows, cols);
        let mut evicted: Vec<u64> = Vec::new();
        for (vid, sink) in self.viewers.iter() {
            if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {
                evicted.push(*vid);
            }
        }
        if !evicted.is_empty() {
            for vid in evicted {
                self.viewers.remove(&vid);
            }
            self.stamp_viewer_count();
        }
    }

    /// Detach `sub` from whatever role it held: clear the controller slot iff it
    /// is the current controller (a no-op if a newer brain took over — a late
    /// disconnect can't unseat the live controller), and remove any viewer rows
    /// owned by `sub`. Leaves `delivered_through` intact (D4-1). Re-stamps
    /// `driven_by` / `viewer_count` when a role actually changed.
    fn detach_if(&mut self, sub: &SharedSend) {
        if let Some(cur) = &self.controller {
            if Arc::ptr_eq(cur, sub) {
                self.controller = None;
                self.controller_by = None;
                self.stamp_driven_by();
            }
        }
        let before = self.viewers.len();
        self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));
        if self.viewers.len() != before {
            self.stamp_viewer_count();
        }
    }

    /// Every sink currently attached (controller + viewers) — the recipients of
    /// an `exit` frame.
    fn all_sinks(&self) -> Vec<SharedSend> {
        let mut v: Vec<SharedSend> = Vec::new();
        if let Some(c) = &self.controller {
            v.push(Arc::clone(c));
        }
        for sink in self.viewers.values() {
            v.push(Arc::clone(&sink.send));
        }
        v
    }

    /// Stamp the perch's `driven_by` to the current controller's identity (the
    /// broker is the single writer — resolves the clear-race). The remote-drive
    /// detection fact (REQ-REACH-1) moved here from `serve_attach` so a displaced
    /// operator's teardown can never wipe the new controller's marker.
    /// Best-effort: a session served without a perch (raw broker tests) has
    /// nowhere to record.
    // [impl->REQ-RCVIEW-1] [impl->REQ-REACH-1]
    fn stamp_driven_by(&self) {
        if self.endpoint.is_empty() {
            return;
        }
        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);
        let _ = spt_store::info::set_driven_by(&perch, self.controller_by.as_deref());
    }

    /// Stamp the perch's `viewer_count` to the live viewer count (broker is the
    /// single writer). Best-effort, like [`OutputLog::stamp_driven_by`].
    // [impl->REQ-RCVIEW-1]
    fn stamp_viewer_count(&self) {
        if self.endpoint.is_empty() {
            return;
        }
        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);
        let _ = spt_store::info::set_viewer_count(&perch, self.viewers.len() as u32);
    }

    /// Whether `sub` is the current controller (the resize-exclusivity check).
    fn is_controller(&self, sub: &SharedSend) -> bool {
        self.controller
            .as_ref()
            .is_some_and(|c| Arc::ptr_eq(c, sub))
    }

    /// The next seq a re-attaching controller still needs (the persisted resume
    /// cursor surfaced in `KIND_SESSIONS` → `resume_seq`).
    fn delivered_through(&self) -> u64 {
        self.delivered_through
    }
}

/// A viewer's dedicated writer thread (REQ-HAZARD-VIEWER-ISOLATION): write the
/// `initial` batch (size frame + ring replay) once, then drain live frames from
/// `rx` with a BLOCKING socket write each. A failed write (dead viewer) ends the
/// thread; the drain thread's next `try_send` then sees `Disconnected` and
/// evicts the row. The drain thread itself never blocks on this socket.
fn viewer_writer(send: SharedSend, initial: Vec<Envelope>, rx: Receiver<Envelope>) {
    for frame in &initial {
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),
            Err(_) => false,
        };
        if !ok {
            return;
        }
    }
    while let Ok(frame) = rx.recv() {
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, &frame).is_ok(),
            Err(_) => false,
        };
        if !ok {
            return;
        }
    }
}

/// A child the broker hosts under a PTY — the §B-owned resources for one
/// session, plus its gapless-resume output log.
struct HostedSession {
    /// The PTY + child (PTY master/writer + spawned child process).
    session: Arc<PtySession>,
    /// The output drain pump (kept so teardown can stop it).
    #[allow(dead_code)] // held for ownership/teardown; not read directly
    drain: Drain,
    /// The sequenced, buffered output + current subscriber.
    log: Arc<Mutex<OutputLog>>,
    /// The owning endpoint (the spawn's label; empty = unlabeled). The
    /// inbound dispatcher's gate subject for remote-drive (D9-1).
    endpoint: String,
}

/// The broker process kernel. Owns the listening socket, the table of hosted
/// sessions, and the broker-owned idempotency anchor (B5); holds no logic state
/// (ADR-0004 §B). The [`EffectJournal`] is a §B resource — the durable
/// recovery anchor a brain crash must not lose, so it lives in the broker, not
/// the brain (Spike #6 constraint 1).
pub struct Broker {
    listener: LocalSocketTransport,
    sessions: Arc<Mutex<HashMap<u64, HostedSession>>>,
    next_id: AtomicU64,
    log_cap_chunks: usize,
    /// The exactly-once anchor for idempotent (`op_id`-carrying) effects.
    journal: Arc<EffectJournal>,
    /// The broker-owned net endpoint + live QUIC connections (D4a, ADR-0004 §B).
    /// Empty when bound without a network host (net-disabled / most tests) OR
    /// when net is still being brought up (REQ-DAEMON-9 boot-race self-heal):
    /// a `OnceLock` so the background net-retry can ATTACH the host after bind
    /// without a relock on the hot effect-dispatch read path (`.get()` is
    /// lock-free). Settable exactly once — the first host wins.
    net: std::sync::OnceLock<NetHost>,
    /// The broker→supervisor planned-restart signal (ADR-0018 D3-3): a
    /// `KIND_BRAIN_RESTART` handler raises it so the brain supervisor cycles the
    /// child onto a freshly-swapped binary. Empty on a bare broker (a test broker
    /// with no supervisor) — the verb is then accepted but unhonored. Set once by
    /// `Daemon::run` after it spawns the supervisor.
    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,
}

impl Broker {
    /// Bind the broker's listening endpoint at the namespaced `name`, with the
    /// effect journal at the default path (`<spt_home>/daemon-effects.log`).
    pub fn bind(name: &str) -> io::Result<Arc<Self>> {
        Self::bind_in(name, default_journal_path())
    }

    /// Bind with an explicit effect-journal path — the testable seam (tests point
    /// the broker-owned anchor at a temp file rather than the shared home).
    pub fn bind_in(name: &str, journal_path: std::path::PathBuf) -> io::Result<Arc<Self>> {
        Self::bind_in_with_net(name, journal_path, None)
    }

    /// Bind with an already-started broker-owned network host (D4a). Taking the
    /// host (not its config) keeps a net bind failure separable from a listener
    /// bind failure — the daemon degrades to a net-less broker rather than
    /// losing PTY hosting (see `Daemon::run`).
    // [impl->REQ-DAEMON-2]
    pub fn bind_in_with_net(
        name: &str,
        journal_path: std::path::PathBuf,
        net: Option<NetHost>,
    ) -> io::Result<Arc<Self>> {
        Self::bind_in_with_net_cap(name, journal_path, net, DEFAULT_LOG_CHUNKS)
    }

    /// [`bind_in_with_net`](Self::bind_in_with_net) with an explicit output-log
    /// ring cap (the per-session eviction bound). Production binds at
    /// [`DEFAULT_LOG_CHUNKS`]; the restoration D4-2 resume harness binds a **tiny**
    /// cap to force ring eviction and exercise the post-eviction floor clamp (a
    /// `resume_seq` below the surviving ring floor must resume forward, losing only
    /// the evicted tail — never gap-erroring the live stream).
    pub fn bind_in_with_net_cap(
        name: &str,
        journal_path: std::path::PathBuf,
        net: Option<NetHost>,
        log_cap_chunks: usize,


Replay logic is sound: `become_controller`/`add_viewer` replay the ring from seq 0; the ring retains all chunks (up to cap) regardless of controller. After the transient spawn-conn drops, `detach_if` clears the controller, so a fresh rc `Control` attach → `become_controller(from_seq=0)` → replays ring. **So if rc gets 0 bytes, the ring is likely EMPTY → the dummy's output never reached the drain.** That's candidate (a)/(b) — the dummy program's stdout under ConPTY. Read the mock dummy mode + the known-good attach.rs test (doyle's STEP-1 reference child).


1	//! `mock-session` — the generic mock adapter's "agent session". A trivial,
2	//! cross-platform stand-in for a real harness session (no `claude`, no `.claude`,
3	//! no CC hooks). When `ManifestRuntime` spawns it from the mock manifest's
4	//! `[session.self]` template, it does what a real session's boot hook would do:
5	//! fire `spt api bind` to flip its perch live, then `spt api state idle`.
6	//!
7	//! It shells out to the `spt` binary exactly as a real adapter would — resolved
8	//! from `--spt <path>`, else `$SPT_BIN`, else `spt` on PATH. This is what proves
9	//! the inbound contract is harness-agnostic: a dumb helper drives it with only
10	//! `spt api` calls.
11
12	use std::process::Command;
13
14	fn arg(args: &[String], flag: &str) -> Option<String> {
15	    args.iter()
16	        .position(|a| a == flag)
17	        .and_then(|i| args.get(i + 1).cloned())
18	}
19
20	fn main() {
21	    let args: Vec<String> = std::env::args().skip(1).collect();
22
23	    // M2b lifecycle stand-in modes. `psyche` is the Psyche companion (spt-core
24	    // binds its nested perch; the child just needs to launch). `echo` is the
25	    // cheap-model echo-commune: read history on stdin, emit a brief on stdout.
26	    match arg(&args, "--mode").as_deref() {
27	        Some("psyche") => {
28	            let id = arg(&args, "--id").unwrap_or_else(|| "mock-psyche".to_string());
29	            println!("MOCK_PSYCHE_UP:{id}");
30	            return;
31	        }
32	        Some("echo") => {
33	            use std::io::Read;
34	            let mut history = String::new();
35	            let _ = std::io::stdin().read_to_string(&mut history);
36	            // A deterministic brief; a real echo summarizes the history it read.
37	            println!("mock echo brief ({} history bytes)", history.len());
38	            return;
39	        }
40	        Some("dummy") => {
41	            // The DUMMY-HARNESS fixture (v0.12.1 real-harness gating): a long-lived
42	            // stand-in for a real harness session. Binds its perch (the harness
43	            // contract) exactly like the default stand-in, then prints a stdout
44	            // heartbeat on an interval and STAYS ALIVE until killed — so the REAL
45	            // `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc`
46	            // attach path can be exercised end-to-end (the default mode exits
47	            // immediately, which is why the v0.12.0 mock tests never caught the
48	            // daemon+broker+PTY lifecycle defects). Isolates "broker wedged /
49	            // endpoint died" from "a real harness failed to launch."
50	            use std::io::Write;
51	            let id = arg(&args, "--id").unwrap_or_else(|| {
52	                eprintln!("mock-session --mode dummy: --id required");
53	                std::process::exit(2);
54	            });
55	            let session_id =
56	                arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
57	            let spt = arg(&args, "--spt")
58	                .or_else(|| std::env::var("SPT_BIN").ok())
59	                .unwrap_or_else(|| "spt".to_string());
60	            let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());
61	            let endpoint_type = arg(&args, "--type");
62	            let heartbeat_ms: u64 = arg(&args, "--heartbeat-ms")
63	                .and_then(|s| s.parse().ok())
64	                .unwrap_or(500);
65
66	            // Bind the perch live (spt-hosted direct bind, mirroring the default
67	            // stand-in below). A failure is fatal — the fixture must come up.
68	            let mut bind_args = vec![
69	                "api".to_string(),
70	                "--adapter".to_string(),
71	                adapter,
72	                "bind".to_string(),
73	                id.clone(),
74	                "--set-session-id".to_string(),
75	                session_id,
76	            ];
77	            if let Some(ty) = &endpoint_type {
78	                bind_args.push("--type".to_string());
79	                bind_args.push(ty.clone());
80	            }
81	            let bind = Command::new(&spt).args(&bind_args).status();
82	            if !matches!(bind, Ok(s) if s.success()) {
83	                eprintln!("mock-session --mode dummy: api bind failed: {bind:?}");
84	                std::process::exit(1);
85	            }
86
87	            println!("DUMMY_HARNESS_READY:{id}");
88	            let _ = std::io::stdout().flush();
89	            // Heartbeat forever — only an external kill (tab-close job reap, daemon
90	            // stop, or an explicit kill) ends it.
91	            let mut n: u64 = 0;
92	            loop {
93	                println!("DUMMY_HARNESS_TICK:{id}:{n}");
94	                let _ = std::io::stdout().flush();
95	                n += 1;
96	                std::thread::sleep(std::time::Duration::from_millis(heartbeat_ms));
97	            }
98	        }
99	        _ => {} // default: the agent-session stand-in below.
100	    }
101
102	    let id = arg(&args, "--id").unwrap_or_else(|| {
103	        eprintln!("mock-session: --id required");
104	        std::process::exit(2);
105	    });
106	    let session_id = arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
107	    let spt = arg(&args, "--spt")
108	        .or_else(|| std::env::var("SPT_BIN").ok())
109	        .unwrap_or_else(|| "spt".to_string());
110	    // Optional endpoint type (the open type system): a non-agent endpoint — e.g.
111	    // a Gateway — binds with `--type gateway`. Absent ⇒ the default live_agent.
112	    let endpoint_type = arg(&args, "--type");
113	    let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());
114
115	    // 1. Post-spawn bind: flip the perch skeleton→live under this session id.
116	    let mut bind_args = vec![
117	        "api".to_string(),
118	        "--adapter".to_string(),
119	        adapter.clone(),
120	        "bind".to_string(),
121	        id.clone(),
122	        "--set-session-id".to_string(),
123	        session_id.clone(),
124	    ];
125	    if let Some(ty) = &endpoint_type {
126	        bind_args.push("--type".to_string());
127	        bind_args.push(ty.clone());
128	    }
129	    let bind = Command::new(&spt).args(&bind_args).status();
130	    if !matches!(bind, Ok(s) if s.success()) {
131	        eprintln!("mock-session: api bind failed: {bind:?}");
132	        std::process::exit(1);
133	    }
134
135	    // 2. Report idle (activity sentinel + echo-gate), authenticating by session.
136	    let state = Command::new(&spt)
137	        .args([
138	            "api",
139	            "--adapter",
140	            &adapter,
141	            "state",
142	            "idle",
143	            &id,
144	            "--session-id",
145	            &session_id,
146	        ])
147	        .status();
148	    if !matches!(state, Ok(s) if s.success()) {
149	        eprintln!("mock-session: api state idle failed: {state:?}");
150	        std::process::exit(1);
151	    }
152
153	    println!("MOCK_SESSION_READY:{id}");
154	}
155

//! `mock-session` — the generic mock adapter's "agent session". A trivial,
//! cross-platform stand-in for a real harness session (no `claude`, no `.claude`,
//! no CC hooks). When `ManifestRuntime` spawns it from the mock manifest's
//! `[session.self]` template, it does what a real session's boot hook would do:
//! fire `spt api bind` to flip its perch live, then `spt api state idle`.
//!
//! It shells out to the `spt` binary exactly as a real adapter would — resolved
//! from `--spt <path>`, else `$SPT_BIN`, else `spt` on PATH. This is what proves
//! the inbound contract is harness-agnostic: a dumb helper drives it with only
//! `spt api` calls.

use std::process::Command;

fn arg(args: &[String], flag: &str) -> Option<String> {
    args.iter()
        .position(|a| a == flag)
        .and_then(|i| args.get(i + 1).cloned())
}

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();

    // M2b lifecycle stand-in modes. `psyche` is the Psyche companion (spt-core
    // binds its nested perch; the child just needs to launch). `echo` is the
    // cheap-model echo-commune: read history on stdin, emit a brief on stdout.
    match arg(&args, "--mode").as_deref() {
        Some("psyche") => {
            let id = arg(&args, "--id").unwrap_or_else(|| "mock-psyche".to_string());
            println!("MOCK_PSYCHE_UP:{id}");
            return;
        }
        Some("echo") => {
            use std::io::Read;
            let mut history = String::new();
            let _ = std::io::stdin().read_to_string(&mut history);
            // A deterministic brief; a real echo summarizes the history it read.
            println!("mock echo brief ({} history bytes)", history.len());
            return;
        }
        Some("dummy") => {
            // The DUMMY-HARNESS fixture (v0.12.1 real-harness gating): a long-lived
            // stand-in for a real harness session. Binds its perch (the harness
            // contract) exactly like the default stand-in, then prints a stdout
            // heartbeat on an interval and STAYS ALIVE until killed — so the REAL
            // `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc`
            // attach path can be exercised end-to-end (the default mode exits
            // immediately, which is why the v0.12.0 mock tests never caught the
            // daemon+broker+PTY lifecycle defects). Isolates "broker wedged /
            // endpoint died" from "a real harness failed to launch."
            use std::io::Write;
            let id = arg(&args, "--id").unwrap_or_else(|| {
                eprintln!("mock-session --mode dummy: --id required");
                std::process::exit(2);
            });
            let session_id =
                arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
            let spt = arg(&args, "--spt")
                .or_else(|| std::env::var("SPT_BIN").ok())
                .unwrap_or_else(|| "spt".to_string());
            let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());
            let endpoint_type = arg(&args, "--type");
            let heartbeat_ms: u64 = arg(&args, "--heartbeat-ms")
                .and_then(|s| s.parse().ok())
                .unwrap_or(500);

            // Bind the perch live (spt-hosted direct bind, mirroring the default
            // stand-in below). A failure is fatal — the fixture must come up.
            let mut bind_args = vec![
                "api".to_string(),
                "--adapter".to_string(),
                adapter,
                "bind".to_string(),
                id.clone(),
                "--set-session-id".to_string(),
                session_id,
            ];
            if let Some(ty) = &endpoint_type {
                bind_args.push("--type".to_string());
                bind_args.push(ty.clone());
            }
            let bind = Command::new(&spt).args(&bind_args).status();
            if !matches!(bind, Ok(s) if s.success()) {
                eprintln!("mock-session --mode dummy: api bind failed: {bind:?}");
                std::process::exit(1);
            }

            println!("DUMMY_HARNESS_READY:{id}");
            let _ = std::io::stdout().flush();
            // Heartbeat forever — only an external kill (tab-close job reap, daemon
            // stop, or an explicit kill) ends it.
            let mut n: u64 = 0;
            loop {
                println!("DUMMY_HARNESS_TICK:{id}:{n}");
                let _ = std::io::stdout().flush();
                n += 1;
                std::thread::sleep(std::time::Duration::from_millis(heartbeat_ms));
            }
        }
        _ => {} // default: the agent-session stand-in below.
    }

    let id = arg(&args, "--id").unwrap_or_else(|| {
        eprintln!("mock-session: --id required");
        std::process::exit(2);
    });
    let session_id = arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
    let spt = arg(&args, "--spt")
        .or_else(|| std::env::var("SPT_BIN").ok())
        .unwrap_or_else(|| "spt".to_string());
    // Optional endpoint type (the open type system): a non-agent endpoint — e.g.
    // a Gateway — binds with `--type gateway`. Absent ⇒ the default live_agent.
    let endpoint_type = arg(&args, "--type");
    let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());

    // 1. Post-spawn bind: flip the perch skeleton→live under this session id.
    let mut bind_args = vec![
        "api".to_string(),
        "--adapter".to_string(),
        adapter.clone(),
        "bind".to_string(),
        id.clone(),
        "--set-session-id".to_string(),
        session_id.clone(),
    ];
    if let Some(ty) = &endpoint_type {
        bind_args.push("--type".to_string());
        bind_args.push(ty.clone());
    }
    let bind = Command::new(&spt).args(&bind_args).status();
    if !matches!(bind, Ok(s) if s.success()) {
        eprintln!("mock-session: api bind failed: {bind:?}");
        std::process::exit(1);
    }

    // 2. Report idle (activity sentinel + echo-gate), authenticating by session.
    let state = Command::new(&spt)
        .args([
            "api",
            "--adapter",
            &adapter,
            "state",
            "idle",
            &id,
            "--session-id",
            &session_id,
        ])
        .status();
    if !matches!(state, Ok(s) if s.success()) {
        eprintln!("mock-session: api state idle failed: {state:?}");
        std::process::exit(1);
    }

    println!("MOCK_SESSION_READY:{id}");
}


Dummy DOES print `DUMMY_HARNESS_TICK` to stdout + flushes (L92-96). So if rc gets 0 bytes and replay is proven sound, the ring is empty → drain never appended, OR rc isn't subscribed to this session's output. Note: dummy shells out `spt api bind` via `.status()` (L81) inheriting the ConPTY before the heartbeat loop. Read the e2e test to see exactly what's asserted + captured.