1	//! The brain (B2) — the restartable logic half that attaches to the broker.
2	//!
3	//! The brain holds all the volatile logic; it may be killed and restarted at
4	//! any moment (a routine self-update swaps it; a crash kills it). It connects to
5	//! the stable [`crate::broker::Broker`] over the versioned IPC, drives sessions
6	//! (spawn / input / resize / kill), and consumes their output. Two invariants
7	//! this type encodes:
8	//!
9	//! - **`gen_start = now()` on cold-start *and* handoff (KNOWN-HAZARDS 2.4).**
10	//!   The wall-clock generation stamp is taken fresh on every start. A handed-off
11	//!   brain rehydrates its resume cursor and generation *counter* from prior
12	//!   state, but its `gen_start` is `now()`, **never** the stale value from that
13	//!   state — a rehydrated `gen_start` would fire time-based discriminators on
14	//!   the new generation. [`Brain::handoff`] takes the prior state by value yet
15	//!   ignores its `gen_start_ms`.
16	//! - **Gapless, exactly-once resume.** The brain tracks `next_seq` — the next
17	//!   output sequence it still needs. On handoff it `subscribe`s from that cursor;
18	//!   the broker replays buffered output from there. [`Brain::read_event`] accepts
19	//!   only the contiguous next `seq` (advancing the cursor), silently drops an
20	//!   already-seen `seq` (a replay duplicate), and treats a forward gap as a hard
21	//!   error — so a lost chunk can never pass silently.
22	// [impl->REQ-HAZARD-GEN-START-NOW]
23	// [impl->REQ-DAEMON-2]
24
25	use std::io;
26	use std::sync::atomic::{AtomicBool, Ordering};
27	use std::time::{SystemTime, UNIX_EPOCH};
28
29	use interprocess::local_socket::Stream;
30
31	use crate::codec::{read_frame, write_frame};
32	use crate::digest::{ingest_chunk, DigestEngine};
33	use crate::digesthub::DigestHub;
34	use crate::frame::{Envelope, Role};
35	use crate::msg::{
36	    decode_bytes, encode_bytes, AppliedEvent, ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq,
37	    NetDialed, NetPresenceEvent, NetPresenceSubscribeReq, NetSent, NetStatusReply, NetStreamData,
38	    NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,
39	    NetStreamsReply, OutputEvent, PairJoinReply, PairJoinReq, ResizeReq, SessionsReply, SpawnReq,
40	    Spawned, SubscribeReq, KIND_APPLIED, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL,
41	    KIND_NET_DIAL, KIND_NET_DIALED, KIND_NET_PRESENCE_EVENT, KIND_NET_PRESENCE_SUBSCRIBE,
42	    KIND_NET_SENT, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS,
43	    KIND_NET_STREAMS_REPLY, KIND_NET_STREAM_DATA, KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN,
44	    KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT,
45	    KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,
46	    KIND_SPAWNED, KIND_SUBSCRIBE,
47	};
48	use crate::transport::{send_hello, LocalSocketTransport};
49
50	/// Wall-clock now in epoch milliseconds — the source for `gen_start` (matches
51	/// the epoch-ms stamping used elsewhere in the tree; no date dependency).
52	pub fn now_ms() -> u64 {
53	    SystemTime::now()
54	        .duration_since(UNIX_EPOCH)
55	        .map(|d| d.as_millis() as u64)
56	        .unwrap_or(0)
57	}
58
59	/// The brain's durable state across a handoff: enough to re-attach gaplessly and
60	/// advance the generation. `gen_start_ms` is recorded for observability but is
61	/// **deliberately not authoritative on restart** — [`Brain::handoff`] sets a
62	/// fresh `gen_start` from `now()` (2.4).
63	#[derive(Debug, Clone)]
64	pub struct BrainState {
65	    /// The session the brain was driving.
66	    pub session_id: u64,
67	    /// The generation counter (incremented on each handoff).
68	    pub generation: u64,
69	    /// The next output sequence still needed (the resume cursor).
70	    pub next_seq: u64,
71	    /// The prior generation's wall-clock start (ignored as authoritative on
72	    /// handoff — kept only to demonstrate it is *not* carried forward).
73	    pub gen_start_ms: u64,
74	}
75
76	/// A demuxed broker→brain event.
77	#[derive(Debug)]
78	pub enum BrokerEvent {
79	    /// A contiguous chunk of a session's output (the cursor has advanced past it).
80	    Output {
81	        session_id: u64,
82	        seq: u64,
83	        bytes: Vec<u8>,
84	    },
85	    /// A spawn succeeded.
86	    Spawned { session_id: u64, pid: Option<u32> },
87	    /// A session's child exited.
88	    Exit { session_id: u64, code: Option<i32> },
89	    /// An idempotent effect is durably applied (or was deduped — `applied_now`
90	    /// distinguishes). The exactly-once ack for a [`Brain::send_effect`] (B5).
91	    Applied {
92	        session_id: u64,
93	        op_id: u64,
94	        applied_now: bool,
95	    },
96	    /// The broker-owned net endpoint's status (a `net-status` reply, D4a).
97	    NetStatus(NetStatusReply),
98	    /// A net dial durably landed — fresh or deduped (D4a).
99	    NetDialed(NetDialed),
100	    /// A stream open durably landed — fresh or deduped (D4b).
101	    NetStreamOpened(NetStreamOpened),
102	    /// A journaled stream send durably landed — fresh or deduped (D4b).
103	    NetSent(NetSent),
104	    /// The broker's held-streams listing (D4b).
105	    NetStreams(NetStreamsReply),
106	    /// The broker's hosted-sessions listing (D9-1: session → endpoint).
107	    Sessions(SessionsReply),
108	    /// The subnet-join ceremony's verdict (a `pair-joined` reply, M7 D3).
109	    PairJoined(PairJoinReply),
110	    /// A contiguous chunk of a stream's reads (the cursor has advanced past it).
111	    NetStreamData {
112	        stream_id: u64,
113	        seq: u64,
114	        bytes: Vec<u8>,
115	    },
116	    /// A stream's read side finished cleanly; `next_seq` is the completeness
117	    /// check (the subscriber's cursor must equal it to have missed nothing).
118	    NetStreamEof { stream_id: u64, next_seq: u64 },
119	    /// A contiguous conn-liveness event (the presence cursor has advanced past
120	    /// it) — connected/disconnected on the broker's conn table (D4c).
121	    NetPresence(NetPresenceEvent),
122	    /// The broker reported a command error.
123	    Error { message: String },
124	    /// A frame of an unrecognized kind (forward-compat: tolerated, surfaced).
125	    Other(Envelope),
126	}
127
128	/// A live brain attached to a broker.
129	pub struct Brain {
130	    conn: Stream,
131	    generation: u64,
132	    gen_start_ms: u64,
133	    session_id: Option<u64>,
134	    /// The next output sequence still needed (resume cursor / dedup floor).
135	    next_seq: u64,
136	    /// Per-net-stream resume cursors (D4b) — the stream-read siblings of
137	    /// `next_seq`, set by [`Brain::net_stream_subscribe`] and advanced by
138	    /// [`Brain::read_event`] under the same accept/dedup/gap discipline.
139	    /// Durability across a handoff is the logic layer's job (a re-attaching
140	    /// brain queries `net-streams` and re-subscribes from its durable cursors).
141	    net_cursors: std::collections::HashMap<u64, u64>,
142	    /// The presence resume cursor (D4c) — the conn-liveness sibling of
143	    /// `net_cursors`, set by [`Brain::net_presence_subscribe`] and advanced by
144	    /// [`Brain::read_event`] under the same accept/dedup/gap discipline.
145	    /// `None` until subscribed (or the first event arrives).
146	    presence_cursor: Option<u64>,
147	}
148
149	impl Brain {
150	    /// Cold start: connect, handshake, fresh generation 0, `gen_start = now`.
151	    pub fn cold_start(name: &str, now_ms: u64) -> io::Result<Self> {
152	        let mut conn = LocalSocketTransport::connect(name)?;
153	        send_hello(&mut conn, Role::Brain)?;
154	        Ok(Brain {
155	            conn,
156	            generation: 0,
157	            gen_start_ms: now_ms,
158	            session_id: None,
159	            next_seq: 0,
160	            net_cursors: std::collections::HashMap::new(),
161	            presence_cursor: None,
162	        })
163	    }
164
165	    /// Handoff: connect, handshake, rehydrate the resume cursor + generation
166	    /// from `prior`, but stamp `gen_start = now()` (NOT `prior.gen_start_ms` —
167	    /// the 2.4 invariant), and re-subscribe to the session from the cursor for a
168	    /// gapless resume.
169	    pub fn handoff(name: &str, prior: BrainState, now_ms: u64) -> io::Result<Self> {
170	        let mut conn = LocalSocketTransport::connect(name)?;
171	        send_hello(&mut conn, Role::Brain)?;
172	        let mut brain = Brain {
173	            conn,
174	            generation: prior.generation + 1,
175	            gen_start_ms: now_ms, // fresh — prior.gen_start_ms is intentionally discarded
176	            session_id: Some(prior.session_id),
177	            next_seq: prior.next_seq,
178	            net_cursors: std::collections::HashMap::new(),
179	            presence_cursor: None,
180	        };
181	        brain.subscribe(prior.session_id, prior.next_seq)?;
182	        Ok(brain)
183	    }
184
185	    /// The wall-clock generation start (epoch ms) — always this generation's own
186	    /// start time, never a rehydrated value.
187	    pub fn gen_start_ms(&self) -> u64 {
188	        self.gen_start_ms
189	    }
190
191	    /// The generation counter (0 on cold start, +1 per handoff).
192	    pub fn generation(&self) -> u64 {
193	        self.generation
194	    }
195
196	    /// The next output sequence still needed (the resume cursor).
197	    pub fn next_seq(&self) -> u64 {
198	        self.next_seq
199	    }
200
201	    /// The session id the brain is driving, if any.
202	    pub fn session_id(&self) -> Option<u64> {
203	        self.session_id
204	    }
205
206	    /// Snapshot durable state for a future handoff.
207	    pub fn snapshot(&self) -> BrainState {
208	        BrainState {
209	            session_id: self.session_id.expect("snapshot before a session exists"),
210	            generation: self.generation,
211	            next_seq: self.next_seq,
212	            gen_start_ms: self.gen_start_ms,
213	        }
214	    }
215
216	    /// Spawn a session under a broker PTY; the broker auto-subscribes this
217	    /// connection. Reads until the `spawned` reply (consuming any output that
218	    /// races ahead).
219	    pub fn spawn_session(&mut self, req: SpawnReq) -> io::Result<u64> {
220	        self.spawn_session_pid(req)
221	            .map(|(session_id, _)| session_id)
222	    }
223
224	    /// [`spawn_session`](Self::spawn_session) that also surfaces the child's
225	    /// OS pid (when the backend exposes one) — measurement/observability
226	    /// callers (the D9-3 dormancy-budget harness) sample the seat by pid.
227	    pub fn spawn_session_pid(&mut self, req: SpawnReq) -> io::Result<(u64, Option<u32>)> {
228	        self.send(
229	            KIND_SPAWN,
230	            serde_json::to_value(req).expect("SpawnReq serializes"),
231	        )?;
232	        loop {
233	            match self.read_event()? {
234	                BrokerEvent::Spawned { session_id, pid } => {
235	                    self.session_id = Some(session_id);
236	                    return Ok((session_id, pid));
237	                }
238	                BrokerEvent::Error { message } => {
239	                    return Err(io::Error::other(message));
240	                }
241	                _ => continue,
242	            }
243	        }
244	    }
245
246	    /// Inject input bytes into the current session (send-keys), the **legacy
247	    /// direct** path: no durable id, no dedup, no ack. For exactly-once delivery
248	    /// of an effect that originates from a durable source (a spool row, a
249	    /// sequenced injection), use [`Brain::send_effect`].
250	    pub fn send_input(&mut self, bytes: &[u8]) -> io::Result<()> {
251	        let sid = self.require_session()?;
252	        self.send(
253	            KIND_INPUT,
254	            serde_json::to_value(InputReq {
255	                session_id: sid,
256	                data_b64: encode_bytes(bytes),
257	                op_id: None,
258	            })
259	            .expect("InputReq serializes"),
260	        )
261	    }
262
263	    /// Inject input **idempotently**: the write carries the durable `op_id` so a
264	    /// replay after a brain crash is deduped at the broker (exactly-once, B5).
265	    /// `op_id` must come from a durable source and be reused verbatim on replay —
266	    /// the same logical effect must always carry the same id, so the broker's
267	    /// applied-set recognizes the re-drive. The broker answers with a
268	    /// [`BrokerEvent::Applied`] for `(session, op_id)`.
269	    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
270	    pub fn send_effect(&mut self, op_id: u64, bytes: &[u8]) -> io::Result<()> {
271	        let sid = self.require_session()?;
272	        self.send(
273	            KIND_INPUT,
274	            serde_json::to_value(InputReq {
275	                session_id: sid,
276	                data_b64: encode_bytes(bytes),
277	                op_id: Some(op_id),
278	            })
279	            .expect("InputReq serializes"),
280	        )
281	    }
282
283	    /// Resize the current session's PTY.
284	    pub fn resize(&mut self, rows: u16, cols: u16) -> io::Result<()> {
285	        let sid = self.require_session()?;
286	        self.send(
287	            KIND_RESIZE,
288	            serde_json::to_value(ResizeReq {
289	                session_id: sid,
290	                rows,
291	                cols,
292	            })
293	            .expect("ResizeReq serializes"),
294	        )
295	    }
296
297	    /// Terminate the current session's child.
298	    pub fn kill_session(&mut self) -> io::Result<()> {
299	        let sid = self.require_session()?;
300	        self.send(
301	            KIND_KILL,
302	            serde_json::to_value(KillReq { session_id: sid }).expect("KillReq serializes"),
303	        )
304	    }
305
306	    /// Read output events until the accumulated bytes contain `needle`,
307	    /// appending all received output to `buf`. Returns the highest seq seen.
308	    /// A session `Exit` before the needle is found is an error (the child died).
309	    pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {
310	        loop {
311	            if contains(buf, needle) {
312	                return Ok(self.next_seq.saturating_sub(1));
313	            }
314	            match self.read_event()? {
315	                BrokerEvent::Output { bytes, .. } => buf.extend_from_slice(&bytes),
316	                BrokerEvent::Exit { .. } => {
317	                    return Err(io::Error::new(
318	                        io::ErrorKind::UnexpectedEof,
319	                        "session exited before the expected output arrived",
320	                    ));
321	                }
322	                BrokerEvent::Error { message } => {
323	                    return Err(io::Error::other(message));
324	                }
325	                _ => {}
326	            }
327	        }
328	    }
329
330	    /// Read and demux one broker event, enforcing exactly-once contiguous output:
331	    /// accept the next `seq` (advancing the cursor), drop an already-seen `seq`
332	    /// (replay duplicate), and reject a forward gap (lost chunk).
333	    pub fn read_event(&mut self) -> io::Result<BrokerEvent> {
334	        loop {
335	            let env = read_frame(&mut self.conn)?;
336	            match env.kind.as_str() {
337	                KIND_OUTPUT => {
338	                    let ev: OutputEvent = serde_json::from_value(env.payload)
339	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
340	                    if ev.seq < self.next_seq {
341	                        continue; // already processed — a replay duplicate; dedup
342	                    }
343	                    if ev.seq > self.next_seq {
344	                        return Err(io::Error::new(
345	                            io::ErrorKind::InvalidData,
346	                            format!("output gap: got seq {} want {}", ev.seq, self.next_seq),
347	                        ));
348	                    }
349	                    self.next_seq = ev.seq + 1;
350	                    let bytes = decode_bytes(&ev.data_b64)
351	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
352	                    return Ok(BrokerEvent::Output {
353	                        session_id: ev.session_id,
354	                        seq: ev.seq,
355	                        bytes,
356	                    });
357	                }
358	                KIND_SPAWNED => {
359	                    let ev: Spawned = serde_json::from_value(env.payload)
360	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
361	                    return Ok(BrokerEvent::Spawned {
362	                        session_id: ev.session_id,
363	                        pid: ev.pid,
364	                    });
365	                }
366	                KIND_EXIT => {
367	                    let ev: ExitEvent = serde_json::from_value(env.payload)
368	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
369	                    return Ok(BrokerEvent::Exit {
370	                        session_id: ev.session_id,
371	                        code: ev.code,
372	                    });
373	                }
374	                KIND_APPLIED => {
375	                    let ev: AppliedEvent = serde_json::from_value(env.payload)
376	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
377	                    return Ok(BrokerEvent::Applied {
378	                        session_id: ev.session_id,
379	                        op_id: ev.op_id,
380	                        applied_now: ev.applied_now,
381	                    });
382	                }
383	                KIND_NET_STATUS_REPLY => {
384	                    let ev: NetStatusReply = serde_json::from_value(env.payload)
385	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
386	                    return Ok(BrokerEvent::NetStatus(ev));
387	                }
388	                KIND_NET_DIALED => {
389	                    let ev: NetDialed = serde_json::from_value(env.payload)
390	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
391	                    return Ok(BrokerEvent::NetDialed(ev));
392	                }
393	                KIND_NET_STREAM_OPENED => {
394	                    let ev: NetStreamOpened = serde_json::from_value(env.payload)
395	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
396	                    return Ok(BrokerEvent::NetStreamOpened(ev));
397	                }
398	                KIND_NET_SENT => {
399	                    let ev: NetSent = serde_json::from_value(env.payload)
400	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
401	                    return Ok(BrokerEvent::NetSent(ev));
402	                }
403	                KIND_NET_STREAMS_REPLY => {
404	                    let ev: NetStreamsReply = serde_json::from_value(env.payload)
405	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
406	                    return Ok(BrokerEvent::NetStreams(ev));
407	                }
408	                KIND_SESSIONS_REPLY => {
409	                    let ev: SessionsReply = serde_json::from_value(env.payload)
410	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
411	                    return Ok(BrokerEvent::Sessions(ev));
412	                }
413	                KIND_PAIR_JOINED => {
414	                    let ev: PairJoinReply = serde_json::from_value(env.payload)
415	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
416	                    return Ok(BrokerEvent::PairJoined(ev));
417	                }
418	                KIND_NET_STREAM_DATA => {
419	                    let ev: NetStreamData = serde_json::from_value(env.payload)
420	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
421	                    // Same exactly-once discipline as PTY output, per stream:
422	                    // accept the contiguous next seq, drop a replay duplicate,
423	                    // reject a forward gap (a lost chunk must not pass silently).
424	                    let cursor = self.net_cursors.entry(ev.stream_id).or_insert(ev.seq);
425	                    if ev.seq < *cursor {
426	                        continue; // replay duplicate — dedup
427	                    }
428	                    if ev.seq > *cursor {
429	                        return Err(io::Error::new(
430	                            io::ErrorKind::InvalidData,
431	                            format!(
432	                                "stream {} gap: got seq {} want {}",
433	                                ev.stream_id, ev.seq, *cursor
434	                            ),
435	                        ));
436	                    }
437	                    *cursor = ev.seq + 1;
438	                    let bytes = decode_bytes(&ev.data_b64)
439	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
440	                    return Ok(BrokerEvent::NetStreamData {
441	                        stream_id: ev.stream_id,
442	                        seq: ev.seq,
443	                        bytes,
444	                    });
445	                }
446	                KIND_NET_STREAM_EOF => {
447	                    let ev: NetStreamEof = serde_json::from_value(env.payload)
448	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
449	                    return Ok(BrokerEvent::NetStreamEof {
450	                        stream_id: ev.stream_id,
451	                        next_seq: ev.next_seq,
452	                    });
453	                }
454	                KIND_NET_PRESENCE_EVENT => {
455	                    let ev: NetPresenceEvent = serde_json::from_value(env.payload)
456	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
457	                    // Same exactly-once discipline as stream reads (D4c):
458	                    // accept the contiguous next seq, drop a replay duplicate,
459	                    // reject a forward gap (a lost liveness event must not
460	                    // pass silently — a phantom "still connected" peer would).
461	                    // [impl->REQ-EP-4]
462	                    let cursor = self.presence_cursor.get_or_insert(ev.seq);
463	                    if ev.seq < *cursor {
464	                        continue; // replay duplicate — dedup
465	                    }
466	                    if ev.seq > *cursor {
467	                        return Err(io::Error::new(
468	                            io::ErrorKind::InvalidData,
469	                            format!("presence gap: got seq {} want {}", ev.seq, *cursor),
470	                        ));
471	                    }
472	                    *cursor = ev.seq + 1;
473	                    return Ok(BrokerEvent::NetPresence(ev));
474	                }
475	                KIND_ERROR => {
476	                    let ev: ErrorEvent = serde_json::from_value(env.payload)
477	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
478	                    return Ok(BrokerEvent::Error {
479	                        message: ev.message,
480	                    });
481	                }
482	                _ => return Ok(BrokerEvent::Other(env)),
483	            }
484	        }
485	    }
486
487	    /// Attach this (freshly-connected) brain to an **existing** broker session's
488	    /// output from `from_seq`, e.g. a dedicated digest-feed subscriber that did
489	    /// not spawn the session itself. Sets the resume cursor + session, then
490	    /// subscribes (the broker replays from `from_seq`, then streams live). Unlike
491	    /// [`Brain::handoff`] this does not touch the generation — it is a *second*
492	    /// reader of a session another brain drives, not a logic handoff.
493	    pub fn attach(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {
494	        self.session_id = Some(session_id);
495	        self.next_seq = from_seq;
496	        self.subscribe(session_id, from_seq)
497	    }
498
499	    /// Run the **PTY-digest feed** for `endpoint_id` (B8): read this session's
500	    /// output events and fold each chunk through `engine`, publishing every change
501	    /// to `hub` (and persisting completed turns when the engine opted in) via
502	    /// [`ingest_chunk`]. This is the brain-side digest projection — the parse is
503	    /// logic, so it lives here, never in the broker (ADR-0004 §B). Returns when
504	    /// the session exits, `stop` is set (checked between events), or the broker
505	    /// connection drops. A non-output event other than `exit` is ignored (the
506	    /// feed only cares about the byte stream).
507	    // [impl->REQ-TERM-4]
508	    pub fn run_digest_feed(
509	        &mut self,
510	        engine: &mut DigestEngine,
511	        hub: &DigestHub,
512	        endpoint_id: &str,
513	        stop: &AtomicBool,
514	    ) -> io::Result<()> {
515	        loop {
516	            if stop.load(Ordering::Relaxed) {
517	                return Ok(());
518	            }
519	            match self.read_event()? {
520	                BrokerEvent::Output { bytes, .. } => ingest_chunk(engine, hub, endpoint_id, &bytes),
521	                BrokerEvent::Exit { .. } => return Ok(()),
522	                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
523	                _ => {}
524	            }
525	        }
526	    }
527
528	    /// Query the broker-owned net endpoint's status (D4a): node id, dialable
529	    /// address, conn count — or `enabled: false` on a net-less broker. Reads
530	    /// until the reply (consuming interleaved events like [`Brain::spawn_session`]).
531	    // [impl->REQ-DAEMON-2]
532	    pub fn net_status(&mut self) -> io::Result<NetStatusReply> {
533	        self.send(KIND_NET_STATUS, serde_json::Value::Null)?;
534	        loop {
535	            match self.read_event()? {
536	                BrokerEvent::NetStatus(s) => return Ok(s),
537	                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
538	                _ => continue,
539	            }
540	        }
541	    }
542
543	    /// Run the joiner leg of the subnet pairing ceremony on the broker (M7
544	    /// D3, REQ-SUBNET-2): the broker owns the node-identity endpoint the
545	    /// ceremony binds and pins. Blocks for the ceremony's bounded life; the
546	    /// reply classifies failures for the CLI's guided UX. Not journaled —
547	    /// the code must never persist, and a re-join is store-idempotent.
548	    // [impl->REQ-SUBNET-2]
549	    pub fn pair_join(
550	        &mut self,
551	        subnet: &str,
552	        code: &str,
553	        responder_addr: Option<serde_json::Value>,
554	    ) -> io::Result<PairJoinReply> {
555	        self.send(
556	            KIND_PAIR_JOIN,
557	            serde_json::to_value(PairJoinReq {
558	                subnet: subnet.to_string(),
559	                code: code.to_string(),
560	                responder_addr,
561	            })
562	            .expect("PairJoinReq serializes"),
563	        )?;
564	        loop {
565	            match self.read_event()? {
566	                BrokerEvent::PairJoined(r) => return Ok(r),
567	                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
568	                _ => continue,
569	            }
570	        }
571	    }
572
573	    /// Dial a peer over the broker-owned endpoint (D4a). `addr` is the peer's
574	    /// serialized `EndpointAddr` (as another node's `net-status` reply carries).
575	    /// With `op_id` the dial is exactly-once: a replay of the same durable id
576	    /// after a brain crash is deduped at the broker and answered with the
577	    /// already-open connection ([`NetDialed::applied_now`] distinguishes).
578	    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
579	    pub fn net_dial(
580	        &mut self,
581	        addr: serde_json::Value,
582	        op_id: Option<u64>,
583	    ) -> io::Result<NetDialed> {
584	        self.send(
585	            KIND_NET_DIAL,
586	            serde_json::to_value(NetDialReq { addr, op_id }).expect("NetDialReq serializes"),
587	        )?;
588	        loop {
589	            match self.read_event()? {
590	                BrokerEvent::NetDialed(d) => return Ok(d),
591	                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
592	                _ => continue,
593	            }
594	        }
595	    }
596
597	    /// Open a bidi stream on a held connection (D4b). With `op_id` the open is
598	    /// exactly-once (a replay is answered with the original stream). NOTE all
599	    /// net op ids — dials, opens, sends — share ONE durable namespace; mint
600	    /// them from a single durable counter.
601	    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
602	    pub fn net_open_stream(
603	        &mut self,
604	        conn_id: u64,
605	        op_id: Option<u64>,
606	    ) -> io::Result<NetStreamOpened> {
607	        self.send(
608	            KIND_NET_STREAM_OPEN,
609	            serde_json::to_value(NetStreamOpenReq { conn_id, op_id })
610	                .expect("NetStreamOpenReq serializes"),
611	        )?;
612	        loop {
613	            match self.read_event()? {
614	                BrokerEvent::NetStreamOpened(ev) => return Ok(ev),
615	                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
616	                _ => continue,
617	            }
618	        }
619	    }
620
621	    /// Write bytes to a held stream over the broker (D4b). With `op_id` the
622	    /// write is exactly-once and this blocks for the `net-sent` ack (returned);
623	    /// without one it is a direct fire-and-forget write (`None` returned).
624	    /// `finish` closes the send side after the write.
625	    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
626	    pub fn net_stream_send(
627	        &mut self,
628	        stream_id: u64,
629	        bytes: &[u8],
630	        op_id: Option<u64>,
631	        finish: bool,
632	    ) -> io::Result<Option<NetSent>> {
633	        self.send(
634	            KIND_NET_STREAM_SEND,
635	            serde_json::to_value(NetStreamSendReq {
636	                stream_id,
637	                data_b64: encode_bytes(bytes),
638	                op_id,
639	                finish,
640	            })
641	            .expect("NetStreamSendReq serializes"),
642	        )?;
643	        if op_id.is_none() {
644	            return Ok(None);
645	        }
646	        loop {
647	            match self.read_event()? {
648	                BrokerEvent::NetSent(ev) if Some(ev.op_id) == op_id => return Ok(Some(ev)),
649	                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
650	                _ => continue,
651	            }
652	        }
653	    }
654
655	    /// List the streams the broker holds (D4b) — how a re-attaching brain
656	    /// finds streams (incl. peer-initiated ones) that arrived while it was down.
657	    pub fn net_streams(&mut self) -> io::Result<NetStreamsReply> {
658	        self.send(KIND_NET_STREAMS, serde_json::Value::Null)?;
659	        loop {
660	            match self.read_event()? {
661	                BrokerEvent::NetStreams(ev) => return Ok(ev),
662	                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
663	                _ => continue,
664	            }
665	        }
666	    }
667
668	    /// List the broker's hosted sessions (id + owning endpoint) — the inbound
669	    /// dispatcher's session→endpoint resolution (D9-1).
670	    pub fn sessions(&mut self) -> io::Result<SessionsReply> {
671	        self.send(KIND_SESSIONS, serde_json::Value::Null)?;
672	        loop {
673	            match self.read_event()? {
674	                BrokerEvent::Sessions(ev) => return Ok(ev),
675	                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
676	                _ => continue,
677	            }
678	        }
679	    }
680
681	    /// (Re)subscribe to a stream's buffered reads from `from_seq`, setting the
682	    /// local cursor (gapless, exactly-once resume — the PTY `subscribe`
683	    /// discipline on a QUIC stream, D4b).
684	    // [impl->REQ-DAEMON-2]
685	    pub fn net_stream_subscribe(&mut self, stream_id: u64, from_seq: u64) -> io::Result<()> {
686	        self.net_cursors.insert(stream_id, from_seq);
687	        self.send(
688	            KIND_NET_STREAM_SUBSCRIBE,
689	            serde_json::to_value(NetStreamSubscribeReq {
690	                stream_id,
691	                from_seq,
692	            })
693	            .expect("NetStreamSubscribeReq serializes"),
694	        )
695	    }
696
697	    /// The next stream-read sequence still needed for `stream_id` (the durable
698	    /// state a handed-off brain carries to resubscribe gaplessly).
699	    pub fn net_cursor(&self, stream_id: u64) -> Option<u64> {
700	        self.net_cursors.get(&stream_id).copied()
701	    }
702
703	    /// (Re)subscribe to the broker's conn-liveness event log from `from_seq`,
704	    /// setting the local cursor (D4c) — gapless, exactly-once presence resume:
705	    /// events produced while no brain was attached replay from the cursor.
706	    // [impl->REQ-EP-4]
707	    pub fn net_presence_subscribe(&mut self, from_seq: u64) -> io::Result<()> {
708	        self.presence_cursor = Some(from_seq);
709	        self.send(
710	            KIND_NET_PRESENCE_SUBSCRIBE,
711	            serde_json::to_value(NetPresenceSubscribeReq { from_seq })
712	                .expect("NetPresenceSubscribeReq serializes"),
713	        )
714	    }
715
716	    /// The next presence sequence still needed (the durable state a handed-off
717	    /// brain carries to resubscribe gaplessly).
718	    pub fn presence_cursor(&self) -> Option<u64> {
719	        self.presence_cursor
720	    }
721
722	    /// Re-attach to a session's output from `from_seq` (handoff resume).
723	    fn subscribe(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {
724	        self.send(
725	            KIND_SUBSCRIBE,
726	            serde_json::to_value(SubscribeReq {
727	                session_id,
728	                from_seq,
729	            })
730	            .expect("SubscribeReq serializes"),
731	        )
732	    }
733
734	    fn send(&mut self, kind: &str, payload: serde_json::Value) -> io::Result<()> {
735	        write_frame(&mut self.conn, &Envelope::new(kind, payload))
736	    }
737
738	    fn require_session(&self) -> io::Result<u64> {
739	        self.session_id
740	            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no active session"))
741	    }
742	}
743
744	/// Whether `haystack` contains the contiguous `needle`.
745	fn contains(haystack: &[u8], needle: &[u8]) -> bool {
746	    if needle.is_empty() {
747	        return true;
748	    }
749	    haystack.windows(needle.len()).any(|w| w == needle)
750	}
751

//! The brain (B2) — the restartable logic half that attaches to the broker.
//!
//! The brain holds all the volatile logic; it may be killed and restarted at
//! any moment (a routine self-update swaps it; a crash kills it). It connects to
//! the stable [`crate::broker::Broker`] over the versioned IPC, drives sessions
//! (spawn / input / resize / kill), and consumes their output. Two invariants
//! this type encodes:
//!
//! - **`gen_start = now()` on cold-start *and* handoff (KNOWN-HAZARDS 2.4).**
//!   The wall-clock generation stamp is taken fresh on every start. A handed-off
//!   brain rehydrates its resume cursor and generation *counter* from prior
//!   state, but its `gen_start` is `now()`, **never** the stale value from that
//!   state — a rehydrated `gen_start` would fire time-based discriminators on
//!   the new generation. [`Brain::handoff`] takes the prior state by value yet
//!   ignores its `gen_start_ms`.
//! - **Gapless, exactly-once resume.** The brain tracks `next_seq` — the next
//!   output sequence it still needs. On handoff it `subscribe`s from that cursor;
//!   the broker replays buffered output from there. [`Brain::read_event`] accepts
//!   only the contiguous next `seq` (advancing the cursor), silently drops an
//!   already-seen `seq` (a replay duplicate), and treats a forward gap as a hard
//!   error — so a lost chunk can never pass silently.
// [impl->REQ-HAZARD-GEN-START-NOW]
// [impl->REQ-DAEMON-2]

use std::io;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};

use interprocess::local_socket::Stream;

use crate::codec::{read_frame, write_frame};
use crate::digest::{ingest_chunk, DigestEngine};
use crate::digesthub::DigestHub;
use crate::frame::{Envelope, Role};
use crate::msg::{
    decode_bytes, encode_bytes, AppliedEvent, ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq,
    NetDialed, NetPresenceEvent, NetPresenceSubscribeReq, NetSent, NetStatusReply, NetStreamData,
    NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,
    NetStreamsReply, OutputEvent, PairJoinReply, PairJoinReq, ResizeReq, SessionsReply, SpawnReq,
    Spawned, SubscribeReq, KIND_APPLIED, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL,
    KIND_NET_DIAL, KIND_NET_DIALED, KIND_NET_PRESENCE_EVENT, KIND_NET_PRESENCE_SUBSCRIBE,
    KIND_NET_SENT, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS,
    KIND_NET_STREAMS_REPLY, KIND_NET_STREAM_DATA, KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN,
    KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT,
    KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,
    KIND_SPAWNED, KIND_SUBSCRIBE,
};
use crate::transport::{send_hello, LocalSocketTransport};

/// Wall-clock now in epoch milliseconds — the source for `gen_start` (matches
/// the epoch-ms stamping used elsewhere in the tree; no date dependency).
pub fn now_ms() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

/// The brain's durable state across a handoff: enough to re-attach gaplessly and
/// advance the generation. `gen_start_ms` is recorded for observability but is
/// **deliberately not authoritative on restart** — [`Brain::handoff`] sets a
/// fresh `gen_start` from `now()` (2.4).
#[derive(Debug, Clone)]
pub struct BrainState {
    /// The session the brain was driving.
    pub session_id: u64,
    /// The generation counter (incremented on each handoff).
    pub generation: u64,
    /// The next output sequence still needed (the resume cursor).
    pub next_seq: u64,
    /// The prior generation's wall-clock start (ignored as authoritative on
    /// handoff — kept only to demonstrate it is *not* carried forward).
    pub gen_start_ms: u64,
}

/// A demuxed broker→brain event.
#[derive(Debug)]
pub enum BrokerEvent {
    /// A contiguous chunk of a session's output (the cursor has advanced past it).
    Output {
        session_id: u64,
        seq: u64,
        bytes: Vec<u8>,
    },
    /// A spawn succeeded.
    Spawned { session_id: u64, pid: Option<u32> },
    /// A session's child exited.
    Exit { session_id: u64, code: Option<i32> },
    /// An idempotent effect is durably applied (or was deduped — `applied_now`
    /// distinguishes). The exactly-once ack for a [`Brain::send_effect`] (B5).
    Applied {
        session_id: u64,
        op_id: u64,
        applied_now: bool,
    },
    /// The broker-owned net endpoint's status (a `net-status` reply, D4a).
    NetStatus(NetStatusReply),
    /// A net dial durably landed — fresh or deduped (D4a).
    NetDialed(NetDialed),
    /// A stream open durably landed — fresh or deduped (D4b).
    NetStreamOpened(NetStreamOpened),
    /// A journaled stream send durably landed — fresh or deduped (D4b).
    NetSent(NetSent),
    /// The broker's held-streams listing (D4b).
    NetStreams(NetStreamsReply),
    /// The broker's hosted-sessions listing (D9-1: session → endpoint).
    Sessions(SessionsReply),
    /// The subnet-join ceremony's verdict (a `pair-joined` reply, M7 D3).
    PairJoined(PairJoinReply),
    /// A contiguous chunk of a stream's reads (the cursor has advanced past it).
    NetStreamData {
        stream_id: u64,
        seq: u64,
        bytes: Vec<u8>,
    },
    /// A stream's read side finished cleanly; `next_seq` is the completeness
    /// check (the subscriber's cursor must equal it to have missed nothing).
    NetStreamEof { stream_id: u64, next_seq: u64 },
    /// A contiguous conn-liveness event (the presence cursor has advanced past
    /// it) — connected/disconnected on the broker's conn table (D4c).
    NetPresence(NetPresenceEvent),
    /// The broker reported a command error.
    Error { message: String },
    /// A frame of an unrecognized kind (forward-compat: tolerated, surfaced).
    Other(Envelope),
}

/// A live brain attached to a broker.
pub struct Brain {
    conn: Stream,
    generation: u64,
    gen_start_ms: u64,
    session_id: Option<u64>,
    /// The next output sequence still needed (resume cursor / dedup floor).
    next_seq: u64,
    /// Per-net-stream resume cursors (D4b) — the stream-read siblings of
    /// `next_seq`, set by [`Brain::net_stream_subscribe`] and advanced by
    /// [`Brain::read_event`] under the same accept/dedup/gap discipline.
    /// Durability across a handoff is the logic layer's job (a re-attaching
    /// brain queries `net-streams` and re-subscribes from its durable cursors).
    net_cursors: std::collections::HashMap<u64, u64>,
    /// The presence resume cursor (D4c) — the conn-liveness sibling of
    /// `net_cursors`, set by [`Brain::net_presence_subscribe`] and advanced by
    /// [`Brain::read_event`] under the same accept/dedup/gap discipline.
    /// `None` until subscribed (or the first event arrives).
    presence_cursor: Option<u64>,
}

impl Brain {
    /// Cold start: connect, handshake, fresh generation 0, `gen_start = now`.
    pub fn cold_start(name: &str, now_ms: u64) -> io::Result<Self> {
        let mut conn = LocalSocketTransport::connect(name)?;
        send_hello(&mut conn, Role::Brain)?;
        Ok(Brain {
            conn,
            generation: 0,
            gen_start_ms: now_ms,
            session_id: None,
            next_seq: 0,
            net_cursors: std::collections::HashMap::new(),
            presence_cursor: None,
        })
    }

    /// Handoff: connect, handshake, rehydrate the resume cursor + generation
    /// from `prior`, but stamp `gen_start = now()` (NOT `prior.gen_start_ms` —
    /// the 2.4 invariant), and re-subscribe to the session from the cursor for a
    /// gapless resume.
    pub fn handoff(name: &str, prior: BrainState, now_ms: u64) -> io::Result<Self> {
        let mut conn = LocalSocketTransport::connect(name)?;
        send_hello(&mut conn, Role::Brain)?;
        let mut brain = Brain {
            conn,
            generation: prior.generation + 1,
            gen_start_ms: now_ms, // fresh — prior.gen_start_ms is intentionally discarded
            session_id: Some(prior.session_id),
            next_seq: prior.next_seq,
            net_cursors: std::collections::HashMap::new(),
            presence_cursor: None,
        };
        brain.subscribe(prior.session_id, prior.next_seq)?;
        Ok(brain)
    }

    /// The wall-clock generation start (epoch ms) — always this generation's own
    /// start time, never a rehydrated value.
    pub fn gen_start_ms(&self) -> u64 {
        self.gen_start_ms
    }

    /// The generation counter (0 on cold start, +1 per handoff).
    pub fn generation(&self) -> u64 {
        self.generation
    }

    /// The next output sequence still needed (the resume cursor).
    pub fn next_seq(&self) -> u64 {
        self.next_seq
    }

    /// The session id the brain is driving, if any.
    pub fn session_id(&self) -> Option<u64> {
        self.session_id
    }

    /// Snapshot durable state for a future handoff.
    pub fn snapshot(&self) -> BrainState {
        BrainState {
            session_id: self.session_id.expect("snapshot before a session exists"),
            generation: self.generation,
            next_seq: self.next_seq,
            gen_start_ms: self.gen_start_ms,
        }
    }

    /// Spawn a session under a broker PTY; the broker auto-subscribes this
    /// connection. Reads until the `spawned` reply (consuming any output that
    /// races ahead).
    pub fn spawn_session(&mut self, req: SpawnReq) -> io::Result<u64> {
        self.spawn_session_pid(req)
            .map(|(session_id, _)| session_id)
    }

    /// [`spawn_session`](Self::spawn_session) that also surfaces the child's
    /// OS pid (when the backend exposes one) — measurement/observability
    /// callers (the D9-3 dormancy-budget harness) sample the seat by pid.
    pub fn spawn_session_pid(&mut self, req: SpawnReq) -> io::Result<(u64, Option<u32>)> {
        self.send(
            KIND_SPAWN,
            serde_json::to_value(req).expect("SpawnReq serializes"),
        )?;
        loop {
            match self.read_event()? {
                BrokerEvent::Spawned { session_id, pid } => {
                    self.session_id = Some(session_id);
                    return Ok((session_id, pid));
                }
                BrokerEvent::Error { message } => {
                    return Err(io::Error::other(message));
                }
                _ => continue,
            }
        }
    }

    /// Inject input bytes into the current session (send-keys), the **legacy
    /// direct** path: no durable id, no dedup, no ack. For exactly-once delivery
    /// of an effect that originates from a durable source (a spool row, a
    /// sequenced injection), use [`Brain::send_effect`].
    pub fn send_input(&mut self, bytes: &[u8]) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_INPUT,
            serde_json::to_value(InputReq {
                session_id: sid,
                data_b64: encode_bytes(bytes),
                op_id: None,
            })
            .expect("InputReq serializes"),
        )
    }

    /// Inject input **idempotently**: the write carries the durable `op_id` so a
    /// replay after a brain crash is deduped at the broker (exactly-once, B5).
    /// `op_id` must come from a durable source and be reused verbatim on replay —
    /// the same logical effect must always carry the same id, so the broker's
    /// applied-set recognizes the re-drive. The broker answers with a
    /// [`BrokerEvent::Applied`] for `(session, op_id)`.
    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
    pub fn send_effect(&mut self, op_id: u64, bytes: &[u8]) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_INPUT,
            serde_json::to_value(InputReq {
                session_id: sid,
                data_b64: encode_bytes(bytes),
                op_id: Some(op_id),
            })
            .expect("InputReq serializes"),
        )
    }

    /// Resize the current session's PTY.
    pub fn resize(&mut self, rows: u16, cols: u16) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_RESIZE,
            serde_json::to_value(ResizeReq {
                session_id: sid,
                rows,
                cols,
            })
            .expect("ResizeReq serializes"),
        )
    }

    /// Terminate the current session's child.
    pub fn kill_session(&mut self) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_KILL,
            serde_json::to_value(KillReq { session_id: sid }).expect("KillReq serializes"),
        )
    }

    /// Read output events until the accumulated bytes contain `needle`,
    /// appending all received output to `buf`. Returns the highest seq seen.
    /// A session `Exit` before the needle is found is an error (the child died).
    pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {
        loop {
            if contains(buf, needle) {
                return Ok(self.next_seq.saturating_sub(1));
            }
            match self.read_event()? {
                BrokerEvent::Output { bytes, .. } => buf.extend_from_slice(&bytes),
                BrokerEvent::Exit { .. } => {
                    return Err(io::Error::new(
                        io::ErrorKind::UnexpectedEof,
                        "session exited before the expected output arrived",
                    ));
                }
                BrokerEvent::Error { message } => {
                    return Err(io::Error::other(message));
                }
                _ => {}
            }
        }
    }

    /// Read and demux one broker event, enforcing exactly-once contiguous output:
    /// accept the next `seq` (advancing the cursor), drop an already-seen `seq`
    /// (replay duplicate), and reject a forward gap (lost chunk).
    pub fn read_event(&mut self) -> io::Result<BrokerEvent> {
        loop {
            let env = read_frame(&mut self.conn)?;
            match env.kind.as_str() {
                KIND_OUTPUT => {
                    let ev: OutputEvent = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    if ev.seq < self.next_seq {
                        continue; // already processed — a replay duplicate; dedup
                    }
                    if ev.seq > self.next_seq {
                        return Err(io::Error::new(
                            io::ErrorKind::InvalidData,
                            format!("output gap: got seq {} want {}", ev.seq, self.next_seq),
                        ));
                    }
                    self.next_seq = ev.seq + 1;
                    let bytes = decode_bytes(&ev.data_b64)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::Output {
                        session_id: ev.session_id,
                        seq: ev.seq,
                        bytes,
                    });
                }
                KIND_SPAWNED => {
                    let ev: Spawned = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::Spawned {
                        session_id: ev.session_id,
                        pid: ev.pid,
                    });
                }
                KIND_EXIT => {
                    let ev: ExitEvent = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::Exit {
                        session_id: ev.session_id,
                        code: ev.code,
                    });
                }
                KIND_APPLIED => {
                    let ev: AppliedEvent = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::Applied {
                        session_id: ev.session_id,
                        op_id: ev.op_id,
                        applied_now: ev.applied_now,
                    });
                }
                KIND_NET_STATUS_REPLY => {
                    let ev: NetStatusReply = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::NetStatus(ev));
                }
                KIND_NET_DIALED => {
                    let ev: NetDialed = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::NetDialed(ev));
                }
                KIND_NET_STREAM_OPENED => {
                    let ev: NetStreamOpened = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::NetStreamOpened(ev));
                }
                KIND_NET_SENT => {
                    let ev: NetSent = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::NetSent(ev));
                }
                KIND_NET_STREAMS_REPLY => {
                    let ev: NetStreamsReply = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::NetStreams(ev));
                }
                KIND_SESSIONS_REPLY => {
                    let ev: SessionsReply = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::Sessions(ev));
                }
                KIND_PAIR_JOINED => {
                    let ev: PairJoinReply = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::PairJoined(ev));
                }
                KIND_NET_STREAM_DATA => {
                    let ev: NetStreamData = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    // Same exactly-once discipline as PTY output, per stream:
                    // accept the contiguous next seq, drop a replay duplicate,
                    // reject a forward gap (a lost chunk must not pass silently).
                    let cursor = self.net_cursors.entry(ev.stream_id).or_insert(ev.seq);
                    if ev.seq < *cursor {
                        continue; // replay duplicate — dedup
                    }
                    if ev.seq > *cursor {
                        return Err(io::Error::new(
                            io::ErrorKind::InvalidData,
                            format!(
                                "stream {} gap: got seq {} want {}",
                                ev.stream_id, ev.seq, *cursor
                            ),
                        ));
                    }
                    *cursor = ev.seq + 1;
                    let bytes = decode_bytes(&ev.data_b64)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::NetStreamData {
                        stream_id: ev.stream_id,
                        seq: ev.seq,
                        bytes,
                    });
                }
                KIND_NET_STREAM_EOF => {
                    let ev: NetStreamEof = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::NetStreamEof {
                        stream_id: ev.stream_id,
                        next_seq: ev.next_seq,
                    });
                }
                KIND_NET_PRESENCE_EVENT => {
                    let ev: NetPresenceEvent = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    // Same exactly-once discipline as stream reads (D4c):
                    // accept the contiguous next seq, drop a replay duplicate,
                    // reject a forward gap (a lost liveness event must not
                    // pass silently — a phantom "still connected" peer would).
                    // [impl->REQ-EP-4]
                    let cursor = self.presence_cursor.get_or_insert(ev.seq);
                    if ev.seq < *cursor {
                        continue; // replay duplicate — dedup
                    }
                    if ev.seq > *cursor {
                        return Err(io::Error::new(
                            io::ErrorKind::InvalidData,
                            format!("presence gap: got seq {} want {}", ev.seq, *cursor),
                        ));
                    }
                    *cursor = ev.seq + 1;
                    return Ok(BrokerEvent::NetPresence(ev));
                }
                KIND_ERROR => {
                    let ev: ErrorEvent = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::Error {
                        message: ev.message,
                    });
                }
                _ => return Ok(BrokerEvent::Other(env)),
            }
        }
    }

    /// Attach this (freshly-connected) brain to an **existing** broker session's
    /// output from `from_seq`, e.g. a dedicated digest-feed subscriber that did
    /// not spawn the session itself. Sets the resume cursor + session, then
    /// subscribes (the broker replays from `from_seq`, then streams live). Unlike
    /// [`Brain::handoff`] this does not touch the generation — it is a *second*
    /// reader of a session another brain drives, not a logic handoff.
    pub fn attach(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {
        self.session_id = Some(session_id);
        self.next_seq = from_seq;
        self.subscribe(session_id, from_seq)
    }

    /// Run the **PTY-digest feed** for `endpoint_id` (B8): read this session's
    /// output events and fold each chunk through `engine`, publishing every change
    /// to `hub` (and persisting completed turns when the engine opted in) via
    /// [`ingest_chunk`]. This is the brain-side digest projection — the parse is
    /// logic, so it lives here, never in the broker (ADR-0004 §B). Returns when
    /// the session exits, `stop` is set (checked between events), or the broker
    /// connection drops. A non-output event other than `exit` is ignored (the
    /// feed only cares about the byte stream).
    // [impl->REQ-TERM-4]
    pub fn run_digest_feed(
        &mut self,
        engine: &mut DigestEngine,
        hub: &DigestHub,
        endpoint_id: &str,
        stop: &AtomicBool,
    ) -> io::Result<()> {
        loop {
            if stop.load(Ordering::Relaxed) {
                return Ok(());
            }
            match self.read_event()? {
                BrokerEvent::Output { bytes, .. } => ingest_chunk(engine, hub, endpoint_id, &bytes),
                BrokerEvent::Exit { .. } => return Ok(()),
                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
                _ => {}
            }
        }
    }

    /// Query the broker-owned net endpoint's status (D4a): node id, dialable
    /// address, conn count — or `enabled: false` on a net-less broker. Reads
    /// until the reply (consuming interleaved events like [`Brain::spawn_session`]).
    // [impl->REQ-DAEMON-2]
    pub fn net_status(&mut self) -> io::Result<NetStatusReply> {
        self.send(KIND_NET_STATUS, serde_json::Value::Null)?;
        loop {
            match self.read_event()? {
                BrokerEvent::NetStatus(s) => return Ok(s),
                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
                _ => continue,
            }
        }
    }

    /// Run the joiner leg of the subnet pairing ceremony on the broker (M7
    /// D3, REQ-SUBNET-2): the broker owns the node-identity endpoint the
    /// ceremony binds and pins. Blocks for the ceremony's bounded life; the
    /// reply classifies failures for the CLI's guided UX. Not journaled —
    /// the code must never persist, and a re-join is store-idempotent.
    // [impl->REQ-SUBNET-2]
    pub fn pair_join(
        &mut self,
        subnet: &str,
        code: &str,
        responder_addr: Option<serde_json::Value>,
    ) -> io::Result<PairJoinReply> {
        self.send(
            KIND_PAIR_JOIN,
            serde_json::to_value(PairJoinReq {
                subnet: subnet.to_string(),
                code: code.to_string(),
                responder_addr,
            })
            .expect("PairJoinReq serializes"),
        )?;
        loop {
            match self.read_event()? {
                BrokerEvent::PairJoined(r) => return Ok(r),
                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
                _ => continue,
            }
        }
    }

    /// Dial a peer over the broker-owned endpoint (D4a). `addr` is the peer's
    /// serialized `EndpointAddr` (as another node's `net-status` reply carries).
    /// With `op_id` the dial is exactly-once: a replay of the same durable id
    /// after a brain crash is deduped at the broker and answered with the
    /// already-open connection ([`NetDialed::applied_now`] distinguishes).
    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
    pub fn net_dial(
        &mut self,
        addr: serde_json::Value,
        op_id: Option<u64>,
    ) -> io::Result<NetDialed> {
        self.send(
            KIND_NET_DIAL,
            serde_json::to_value(NetDialReq { addr, op_id }).expect("NetDialReq serializes"),
        )?;
        loop {
            match self.read_event()? {
                BrokerEvent::NetDialed(d) => return Ok(d),
                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
                _ => continue,
            }
        }
    }

    /// Open a bidi stream on a held connection (D4b). With `op_id` the open is
    /// exactly-once (a replay is answered with the original stream). NOTE all
    /// net op ids — dials, opens, sends — share ONE durable namespace; mint
    /// them from a single durable counter.
    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
    pub fn net_open_stream(
        &mut self,
        conn_id: u64,
        op_id: Option<u64>,
    ) -> io::Result<NetStreamOpened> {
        self.send(
            KIND_NET_STREAM_OPEN,
            serde_json::to_value(NetStreamOpenReq { conn_id, op_id })
                .expect("NetStreamOpenReq serializes"),
        )?;
        loop {
            match self.read_event()? {
                BrokerEvent::NetStreamOpened(ev) => return Ok(ev),
                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
                _ => continue,
            }
        }
    }

    /// Write bytes to a held stream over the broker (D4b). With `op_id` the
    /// write is exactly-once and this blocks for the `net-sent` ack (returned);
    /// without one it is a direct fire-and-forget write (`None` returned).
    /// `finish` closes the send side after the write.
    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
    pub fn net_stream_send(
        &mut self,
        stream_id: u64,
        bytes: &[u8],
        op_id: Option<u64>,
        finish: bool,
    ) -> io::Result<Option<NetSent>> {
        self.send(
            KIND_NET_STREAM_SEND,
            serde_json::to_value(NetStreamSendReq {
                stream_id,
                data_b64: encode_bytes(bytes),
                op_id,
                finish,
            })
            .expect("NetStreamSendReq serializes"),
        )?;
        if op_id.is_none() {
            return Ok(None);
        }
        loop {
            match self.read_event()? {
                BrokerEvent::NetSent(ev) if Some(ev.op_id) == op_id => return Ok(Some(ev)),
                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
                _ => continue,
            }
        }
    }

    /// List the streams the broker holds (D4b) — how a re-attaching brain
    /// finds streams (incl. peer-initiated ones) that arrived while it was down.
    pub fn net_streams(&mut self) -> io::Result<NetStreamsReply> {
        self.send(KIND_NET_STREAMS, serde_json::Value::Null)?;
        loop {
            match self.read_event()? {
                BrokerEvent::NetStreams(ev) => return Ok(ev),
                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
                _ => continue,
            }
        }
    }

    /// List the broker's hosted sessions (id + owning endpoint) — the inbound
    /// dispatcher's session→endpoint resolution (D9-1).
    pub fn sessions(&mut self) -> io::Result<SessionsReply> {
        self.send(KIND_SESSIONS, serde_json::Value::Null)?;
        loop {
            match self.read_event()? {
                BrokerEvent::Sessions(ev) => return Ok(ev),
                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
                _ => continue,
            }
        }
    }

    /// (Re)subscribe to a stream's buffered reads from `from_seq`, setting the
    /// local cursor (gapless, exactly-once resume — the PTY `subscribe`
    /// discipline on a QUIC stream, D4b).
    // [impl->REQ-DAEMON-2]
    pub fn net_stream_subscribe(&mut self, stream_id: u64, from_seq: u64) -> io::Result<()> {
        self.net_cursors.insert(stream_id, from_seq);
        self.send(
            KIND_NET_STREAM_SUBSCRIBE,
            serde_json::to_value(NetStreamSubscribeReq {
                stream_id,
                from_seq,
            })
            .expect("NetStreamSubscribeReq serializes"),
        )
    }

    /// The next stream-read sequence still needed for `stream_id` (the durable
    /// state a handed-off brain carries to resubscribe gaplessly).
    pub fn net_cursor(&self, stream_id: u64) -> Option<u64> {
        self.net_cursors.get(&stream_id).copied()
    }

    /// (Re)subscribe to the broker's conn-liveness event log from `from_seq`,
    /// setting the local cursor (D4c) — gapless, exactly-once presence resume:
    /// events produced while no brain was attached replay from the cursor.
    // [impl->REQ-EP-4]
    pub fn net_presence_subscribe(&mut self, from_seq: u64) -> io::Result<()> {
        self.presence_cursor = Some(from_seq);
        self.send(
            KIND_NET_PRESENCE_SUBSCRIBE,
            serde_json::to_value(NetPresenceSubscribeReq { from_seq })
                .expect("NetPresenceSubscribeReq serializes"),
        )
    }

    /// The next presence sequence still needed (the durable state a handed-off
    /// brain carries to resubscribe gaplessly).
    pub fn presence_cursor(&self) -> Option<u64> {
        self.presence_cursor
    }

    /// Re-attach to a session's output from `from_seq` (handoff resume).
    fn subscribe(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {
        self.send(
            KIND_SUBSCRIBE,
            serde_json::to_value(SubscribeReq {
                session_id,
                from_seq,
            })
            .expect("SubscribeReq serializes"),
        )
    }

    fn send(&mut self, kind: &str, payload: serde_json::Value) -> io::Result<()> {
        write_frame(&mut self.conn, &Envelope::new(kind, payload))
    }

    fn require_session(&self) -> io::Result<u64> {
        self.session_id
            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no active session"))
    }
}

/// Whether `haystack` contains the contiguous `needle`.
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
    if needle.is_empty() {
        return true;
    }
    haystack.windows(needle.len()).any(|w| w == needle)
}