1240	    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]
1241	    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {
1242	        let req: SpawnReq =
1243	            serde_json::from_value(env.payload).map_err(|e| format!("bad spawn payload: {e}"))?;
1244	        // REQ-HOST-RUN-2: spawn in the requested project cwd when carried;
1245	        // `None` inherits the broker's cwd (legacy/test + W1 bringup shortcut).
1246	        // Inject the manifest [env] vars (REQ-HAZARD-ENV-SUBST) on top of the
1247	        // inherited env — empty for a legacy/N-1 spawn (inherit only).
1248	        let session = PtySession::spawn_program_in_env(
1249	            &req.program,
1250	            &req.args,
1251	            SurfaceSize::new(req.rows, req.cols),
1252	            req.cwd.as_deref(),
1253	            req.env.iter().map(|(k, v)| (k.as_str(), v.as_str())),
1254	        )
1255	        .map_err(|e| format!("spawn failed: {e}"))?;
1256	        let session = Arc::new(session);
1257	        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
1258	        let pid = session.process_id();
1259
1260	        // The output log, with the spawning connection pre-attached as the
1261	        // controller (identity `None` = LOCAL, not a remote driver) so no chunk
1262	        // is missed before an explicit subscribe — and so a remote attach to a
1263	        // spawned-but-undriven session takes control silently (no loud displace
1264	        // of the local-spawner placeholder; REQ-RCVIEW-1).
1265	        let log = Arc::new(Mutex::new(OutputLog::new(
1266	            id,
1267	            self.log_cap_chunks,
1268	            req.endpoint.clone(),
1269	            (req.rows, req.cols),
1270	        )));
1271	        log.lock()
1272	            .unwrap()
1273	            .become_controller(Arc::clone(send), None, 0);
1274
1275	        // Output drain → append under the log lock (fast: ring + viewer fan-out),
1276	        // then deliver to the controller OFF the lock with a bounded deadline
1277	        // (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). The controller write is no
1278	        // longer inline under the log lock, so a backed-up controller can never
1279	        // wedge the session; a fully-stalled controller is detached after the
1280	        // deadline (bounded wedge, never permanent).
1281	        let log_drain = Arc::clone(&log);
1282	        let drain = session
1283	            .drain(move |chunk| {
1284	                let job = log_drain.lock().unwrap().append(chunk);
1285	                if let Some(job) = job {
1286	                    if let Err(epoch) = job.deliver() {
1287	                        log_drain.lock().unwrap().mark_controller_gone(epoch);
1288	                    }
1289	                }
1290	            })
1291	            .map_err(|e| format!("drain failed: {e}"))?;
1292
1293	        // Exit waiter → one `exit` frame when the child is reaped, then the dead
1294	        // session is removed from the table. Runs on its own thread; `wait`
1295	        // holds the child lock but `kill` uses a split killer.
1296	        let sessions_exit = Arc::clone(&self.sessions);
1297	        let log_exit = Arc::clone(&log);
1298	        let waiter = Arc::clone(&session);
1299	        thread::spawn(move || {
1300	            if let Ok(status) = waiter.wait() {
1301	                let ev = ExitEvent {
1302	                    session_id: id,
1303	                    code: Some(status.exit_code() as i32),
1304	                };
1305	                let frame = Envelope::new(
1306	                    KIND_EXIT,
1307	                    serde_json::to_value(ev).expect("ExitEvent serializes"),
1308	                );
1309	                // Send to every attached sink — controller + all viewers (under
1310	                // the log lock).
1311	                let sinks = log_exit.lock().unwrap().all_sinks();
1312	                for sink in sinks {
1313	                    if let Ok(mut s) = sink.lock() {
1314	                        let _ = write_frame(&mut *s, &frame);
1315	                    }
1316	                }
1317	            }
1318	            // Reap the dead session from the table.
1319	            sessions_exit.lock().unwrap().remove(&id);
1320	        });
1321
1322	        // Layer C: if the spawn carried a translation binary, bring it up with the
1323	        // session — spawn the pure stdin→stdout filter + the inject worker that
1324	        // owns the atomic PTY apply. A spawn failure FAILS CLOSED to raw inject
1325	        // (logged, `None`) rather than aborting the session — a missing binary must
1326	        // never cost the operator their agent.
1327	        let translation = req.translation_binary.as_deref().and_then(|path| {
1328	            let (cmd_tx, cmd_rx) = channel::<KeyCmd>();
1329	            match TranslationChild::spawn(path, cmd_tx) {
1330	                Ok(child) => {
1331	                    let child = Arc::new(child);
1332	                    // First line: identity (content-free of any PTY bytes).
1333	                    let _ = child.send(&ToBinary::Init {
1334	                        endpoint_id: req.endpoint.clone(),
1335	                        node: spt_store::hostlabel::os_hostname().unwrap_or_default(),
1336	                    });
1337	                    let floor = Arc::new(Mutex::new(InjectFloor::default()));
1338	                    let faulted = Arc::new(AtomicBool::new(false));
1339	                    let (event_tx, event_rx) = channel::<String>();
1340	                    let w_session = Arc::clone(&session);
1341	                    let w_child = Arc::clone(&child);
1342	                    let w_floor = Arc::clone(&floor);
1343	                    let w_faulted = Arc::clone(&faulted);
1344	                    thread::spawn(move || {
1345	                        run_inject_worker(w_session, w_child, w_floor, event_rx, cmd_rx, w_faulted);
1346	                    });
1347	                    Some(Arc::new(Translation {
1348	                        floor,
1349	                        event_tx,
1350	                        child,
1351	                        faulted,
1352	                    }))
1353	                }
1354	                Err(e) => {
1355	                    eprintln!(
1356	                        "TRANSLATION_SPAWN_FAILED:{path}: {e} — degrading to raw inject"
1357	                    );
1358	                    None
1359	                }
1360	            }
1361	        });
1362
1363	        self.sessions.lock().unwrap().insert(
1364	            id,
1365	            HostedSession {
1366	                session,
1367	                drain,
1368	                log,
1369	                endpoint: req.endpoint.clone(),
1370	                translation,
1371	            },
1372	        );
1373
1374	        let resp = Spawned {
1375	            session_id: id,
1376	            pid,
1377	        };
1378	        let frame = Envelope::new(
1379	            KIND_SPAWNED,
1380	            serde_json::to_value(resp).expect("Spawned serializes"),
1381	        );
1382	        send_frame(send, &frame);
1383	        Ok(id)
1384	    }
1385
1386	    /// Re-attach a brain to a session's output under the W2.5 controller/viewer
1387	    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,
1388	    /// exactly-once resume. The `intent` + `by` select the role; the broker
1389	    /// resolves it (controller take / viewer add / busy-refuse / loud displace)
1390	    /// and replies the [`SubscribeOutcome`] so the serving brain can wake
1391	    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A
1392	    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7
1393	    /// self-kick guard.
1394	    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {
1395	        let req: SubscribeReq = serde_json::from_value(env.payload)
1396	            .map_err(|e| format!("bad subscribe payload: {e}"))?;
1397	        // Clone the log Arc out from under the sessions lock, then resolve under
1398	        // the log lock — never hold both locks at once.
1399	        //

    // [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 → append under the log lock (fast: ring + viewer fan-out),
        // then deliver to the controller OFF the lock with a bounded deadline
        // (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). The controller write is no
        // longer inline under the log lock, so a backed-up controller can never
        // wedge the session; a fully-stalled controller is detached after the
        // deadline (bounded wedge, never permanent).
        let log_drain = Arc::clone(&log);
        let drain = session
            .drain(move |chunk| {
                let job = log_drain.lock().unwrap().append(chunk);
                if let Some(job) = job {
                    if let Err(epoch) = job.deliver() {
                        log_drain.lock().unwrap().mark_controller_gone(epoch);
                    }
                }
            })
            .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);
        });

        // Layer C: if the spawn carried a translation binary, bring it up with the
        // session — spawn the pure stdin→stdout filter + the inject worker that
        // owns the atomic PTY apply. A spawn failure FAILS CLOSED to raw inject
        // (logged, `None`) rather than aborting the session — a missing binary must
        // never cost the operator their agent.
        let translation = req.translation_binary.as_deref().and_then(|path| {
            let (cmd_tx, cmd_rx) = channel::<KeyCmd>();
            match TranslationChild::spawn(path, cmd_tx) {
                Ok(child) => {
                    let child = Arc::new(child);
                    // First line: identity (content-free of any PTY bytes).
                    let _ = child.send(&ToBinary::Init {
                        endpoint_id: req.endpoint.clone(),
                        node: spt_store::hostlabel::os_hostname().unwrap_or_default(),
                    });
                    let floor = Arc::new(Mutex::new(InjectFloor::default()));
                    let faulted = Arc::new(AtomicBool::new(false));
                    let (event_tx, event_rx) = channel::<String>();
                    let w_session = Arc::clone(&session);
                    let w_child = Arc::clone(&child);
                    let w_floor = Arc::clone(&floor);
                    let w_faulted = Arc::clone(&faulted);
                    thread::spawn(move || {
                        run_inject_worker(w_session, w_child, w_floor, event_rx, cmd_rx, w_faulted);
                    });
                    Some(Arc::new(Translation {
                        floor,
                        event_tx,
                        child,
                        faulted,
                    }))
                }
                Err(e) => {
                    eprintln!(
                        "TRANSLATION_SPAWN_FAILED:{path}: {e} — degrading to raw inject"
                    );
                    None
                }
            }
        });

        self.sessions.lock().unwrap().insert(
            id,
            HostedSession {
                session,
                drain,
                log,
                endpoint: req.endpoint.clone(),
                translation,
            },
        );

        let resp = Spawned {
            session_id: id,
            pid,
        };
        let frame = Envelope::new(
            KIND_SPAWNED,
            serde_json::to_value(resp).expect("Spawned serializes"),
        );
        send_frame(send, &frame);
        Ok(id)
    }

    /// Re-attach a brain to a session's output under the W2.5 controller/viewer
    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,
    /// exactly-once resume. The `intent` + `by` select the role; the broker
    /// resolves it (controller take / viewer add / busy-refuse / loud displace)
    /// and replies the [`SubscribeOutcome`] so the serving brain can wake
    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A
    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7
    /// self-kick guard.
    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {
        let req: SubscribeReq = serde_json::from_value(env.payload)
            .map_err(|e| format!("bad subscribe payload: {e}"))?;
        // Clone the log Arc out from under the sessions lock, then resolve under
        // the log lock — never hold both locks at once.
        //


Read pty.rs write_input + surface, and spt_store::info API for the backpressure stamp.


220	            Ok(())
221	        }
222	        #[cfg(unix)]
223	        {
224	            res.map_err(SurfaceError::Io)
225	        }
226	    }
227
228	    /// The child's OS process id, if the backend exposes one.
229	    pub fn process_id(&self) -> Option<u32> {
230	        self.pid
231	    }
232	}
233
234	// [impl->REQ-TERM-1]
235	impl SessionSurface for PtySession {
236	    fn write_input(&self, bytes: &[u8]) -> Result<(), SurfaceError> {
237	        let mut w = self.writer.lock().unwrap();
238	        w.write_all(bytes).map_err(SurfaceError::Io)?;
239	        w.flush().map_err(SurfaceError::Io)
240	    }
241
242	    fn resize(&self, size: SurfaceSize) -> Result<(), SurfaceError> {
243	        // Hold the master lock so resize never races a concurrent reader/resize
244	        // (Spike #5: resize races the reader on Windows).
245	        self.master
246	            .lock()
247	            .unwrap()
248	            .resize(size.into())
249	            .map_err(|e| SurfaceError::Io(io_other(e)))
250	    }
251	}
252
253	/// Fold a `portable-pty` error (a private `anyhow::Error` alias) into a
254	/// `std::io::Error` so the crate's public surface never leaks `anyhow`. Generic
255	/// over `Display` to avoid naming the un-exported error type.
256	fn io_other<E: std::fmt::Display>(e: E) -> std::io::Error {
257	    std::io::Error::other(e.to_string())
258	}
259

            Ok(())
        }
        #[cfg(unix)]
        {
            res.map_err(SurfaceError::Io)
        }
    }

    /// The child's OS process id, if the backend exposes one.
    pub fn process_id(&self) -> Option<u32> {
        self.pid
    }
}

// [impl->REQ-TERM-1]
impl SessionSurface for PtySession {
    fn write_input(&self, bytes: &[u8]) -> Result<(), SurfaceError> {
        let mut w = self.writer.lock().unwrap();
        w.write_all(bytes).map_err(SurfaceError::Io)?;
        w.flush().map_err(SurfaceError::Io)
    }

    fn resize(&self, size: SurfaceSize) -> Result<(), SurfaceError> {
        // Hold the master lock so resize never races a concurrent reader/resize
        // (Spike #5: resize races the reader on Windows).
        self.master
            .lock()
            .unwrap()
            .resize(size.into())
            .map_err(|e| SurfaceError::Io(io_other(e)))
    }
}

/// Fold a `portable-pty` error (a private `anyhow::Error` alias) into a
/// `std::io::Error` so the crate's public surface never leaks `anyhow`. Generic
/// over `Display` to avoid naming the un-exported error type.
fn io_other<E: std::fmt::Display>(e: E) -> std::io::Error {
    std::io::Error::other(e.to_string())
}


Grep

pub fn set_driven_by|pub fn set_viewer_count|pub fn set_


crates\spt-store\src\attachment.rs:81:    pub fn set_detached(&mut self, name: &str, detached: bool) -> bool {
crates\spt-store\src\info.rs:252:pub fn set_status(perch_path: &Path, status: &str) -> std::io::Result<()> {
crates\spt-store\src\info.rs:259:pub fn set_last_active(perch_path: &Path, now_ms: u64) -> std::io::Result<()> {
crates\spt-store\src\info.rs:268:pub fn set_rest_state(
crates\spt-store\src\info.rs:282:pub fn set_resources(perch_path: &Path, blurb: Option<&str>) -> std::io::Result<()> {
crates\spt-store\src\info.rs:290:pub fn set_driven_by(perch_path: &Path, node: Option<&str>) -> std::io::Result<()> {
crates\spt-store\src\info.rs:298:pub fn set_viewer_count(perch_path: &Path, count: u32) -> std::io::Result<()> {
crates\spt-store\src\info.rs:319:pub fn set_psyche_host_error(perch_path: &Path, reason: Option<&str>) -> std::io::Result<()> {
crates\spt-store\src\visibility.rs:96:    pub fn set_default_hide(&mut self, endpoint: &str, hide: bool) {
crates\spt-store\src\visibility.rs:122:    pub fn set_override(&mut self, endpoint: &str, subnet: &str, hidden: Option<bool>) {
crates\spt-store\src\visibility.rs:162:    pub fn set_sync_subnets(&mut self, endpoint: &str, subnets: Vec<String>) {
crates\spt-store\src\subnet.rs:270:    pub fn set_hide_new_endpoints(&mut self, name: &str, hide: bool) -> Result<(), SubnetError> {


240	    let mut rec = read_info(perch_path).ok_or_else(|| {
241	        std::io::Error::new(std::io::ErrorKind::NotFound, "info.json absent or unreadable")
242	    })?;
243	    mutate(&mut rec);
244	    write_info(perch_path, &rec)
245	}
246
247	/// Set the daemon-managed `status` (the 2.5 daemon-authoritative liveness
248	/// marker). Serialized via [`mutate_info`] so a concurrent recency/rest stamp
249	/// never clobbers it. Errors if the record is absent — the daemon only marks
250	/// perches it bound.
251	// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]
252	pub fn set_status(perch_path: &Path, status: &str) -> std::io::Result<()> {
253	    mutate_info(perch_path, |rec| rec.status = Some(status.to_string()))
254	}
255
256	/// Stamp the `last_active_ms` recency marker (REQ-UPD-4). Serialized via
257	/// [`mutate_info`].
258	// [impl->REQ-UPD-4]
259	pub fn set_last_active(perch_path: &Path, now_ms: u64) -> std::io::Result<()> {
260	    mutate_info(perch_path, |rec| rec.last_active_ms = Some(now_ms))
261	}
262
263	/// Stamp the daemon-owned resting state (D9-2, REQ-INST-3). `dormant_since_ms`
264	/// (the auto-suspend anchor) moves with it atomically — `Some` exactly when
265	/// `state` is dormant, so a reader never sees a dormant record without its anchor.
266	/// Serialized via [`mutate_info`].
267	// [impl->REQ-INST-3]
268	pub fn set_rest_state(
269	    perch_path: &Path,
270	    state: &str,
271	    dormant_since_ms: Option<u64>,
272	) -> std::io::Result<()> {
273	    mutate_info(perch_path, |rec| {
274	        rec.rest_state = Some(state.to_string());
275	        rec.dormant_since_ms = dormant_since_ms;
276	    })
277	}
278
279	/// Set (or clear, with `None`) the endpoint-authored resource blurb (D9-4,
280	/// REQ-INST-14) — the `spt resources set` write. Serialized via [`mutate_info`].
281	// [impl->REQ-INST-14]
282	pub fn set_resources(perch_path: &Path, blurb: Option<&str>) -> std::io::Result<()> {
283	    mutate_info(perch_path, |rec| rec.resources = blurb.map(str::to_string))
284	}
285
286	/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker (REQ-REACH-1):
287	/// which node's operator is driving this endpoint's session. Serialized via
288	/// [`mutate_info`].
289	// [impl->REQ-REACH-1]
290	pub fn set_driven_by(perch_path: &Path, node: Option<&str>) -> std::io::Result<()> {
291	    mutate_info(perch_path, |rec| rec.driven_by = node.map(str::to_string))
292	}
293
294	/// Stamp the read-only `viewer_count` (REQ-RCVIEW-1). The broker is the single
295	/// logical writer (it owns the viewers map); `0` clears the field. Serialized via
296	/// [`mutate_info`].
297	// [impl->REQ-RCVIEW-1]
298	pub fn set_viewer_count(perch_path: &Path, count: u32) -> std::io::Result<()> {
299	    mutate_info(perch_path, |rec| {
300	        rec.viewer_count = if count == 0 { None } else { Some(count) }
301	    })
302	}
303
304	/// Stamp (`Some(reason)`) or clear (`None`) the psyche-host-failure state on a
305	/// live perch (v0.8.1, REQ-HAZARD-LIVEHOST-BOOT-RACE) — the harness-reachable
306	/// mirror of the brain's invisible `LIVEHOST_PSYCHE_FAIL` stderr line.
307	///
308	/// - `Some(reason)` — set/overwrite the CURRENT-STATE stamp: `reason` is the
309	///   `spawn_psyche` error string, `ts` is now ([`crate::timefmt::now_rfc3339`]
310	///   — the same clock every other info.json string stamp uses), and `attempts`
311	///   is the prior `attempts + 1` (read inside the locked RMW so concurrent
312	///   retries can't lose a count), `1` on the first failure.
313	/// - `None` — clear it (a later successful host shows clean).
314	///
315	/// `status` is **never** touched — daemon liveness stays authoritative
316	/// (KH 2.5). Serialized via [`mutate_info`]; errors if the record is absent —
317	/// the daemon only stamps perches it hosts.
318	// [impl->REQ-HAZARD-LIVEHOST-BOOT-RACE]
319	pub fn set_psyche_host_error(perch_path: &Path, reason: Option<&str>) -> std::io::Result<()> {
320	    mutate_info(perch_path, |rec| match reason {
321	        Some(r) => {
322	            let attempts = rec
323	                .psyche_host_error
324	                .as_ref()
325	                .map(|e| e.attempts.saturating_add(1))
326	                .unwrap_or(1);
327	            rec.psyche_host_error = Some(PsycheHostError {
328	                reason: r.to_string(),
329	                ts: crate::timefmt::now_rfc3339(),
330	                attempts,
331	            });
332	        }
333	        None => rec.psyche_host_error = None,
334	    })
335	}
336
337	/// Number of read attempts before giving up (torn-read retry).
338	const READ_ATTEMPTS: u32 = 5;
339

    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(std::io::ErrorKind::NotFound, "info.json absent or unreadable")
    })?;
    mutate(&mut rec);
    write_info(perch_path, &rec)
}

/// Set the daemon-managed `status` (the 2.5 daemon-authoritative liveness
/// marker). Serialized via [`mutate_info`] so a concurrent recency/rest stamp
/// never clobbers it. Errors if the record is absent — the daemon only marks
/// perches it bound.
// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]
pub fn set_status(perch_path: &Path, status: &str) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| rec.status = Some(status.to_string()))
}

/// Stamp the `last_active_ms` recency marker (REQ-UPD-4). Serialized via
/// [`mutate_info`].
// [impl->REQ-UPD-4]
pub fn set_last_active(perch_path: &Path, now_ms: u64) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| rec.last_active_ms = Some(now_ms))
}

/// Stamp the daemon-owned resting state (D9-2, REQ-INST-3). `dormant_since_ms`
/// (the auto-suspend anchor) moves with it atomically — `Some` exactly when
/// `state` is dormant, so a reader never sees a dormant record without its anchor.
/// Serialized via [`mutate_info`].
// [impl->REQ-INST-3]
pub fn set_rest_state(
    perch_path: &Path,
    state: &str,
    dormant_since_ms: Option<u64>,
) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| {
        rec.rest_state = Some(state.to_string());
        rec.dormant_since_ms = dormant_since_ms;
    })
}

/// Set (or clear, with `None`) the endpoint-authored resource blurb (D9-4,
/// REQ-INST-14) — the `spt resources set` write. Serialized via [`mutate_info`].
// [impl->REQ-INST-14]
pub fn set_resources(perch_path: &Path, blurb: Option<&str>) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| rec.resources = blurb.map(str::to_string))
}

/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker (REQ-REACH-1):
/// which node's operator is driving this endpoint's session. Serialized via
/// [`mutate_info`].
// [impl->REQ-REACH-1]
pub fn set_driven_by(perch_path: &Path, node: Option<&str>) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| rec.driven_by = node.map(str::to_string))
}

/// Stamp the read-only `viewer_count` (REQ-RCVIEW-1). The broker is the single
/// logical writer (it owns the viewers map); `0` clears the field. Serialized via
/// [`mutate_info`].
// [impl->REQ-RCVIEW-1]
pub fn set_viewer_count(perch_path: &Path, count: u32) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| {
        rec.viewer_count = if count == 0 { None } else { Some(count) }
    })
}

/// Stamp (`Some(reason)`) or clear (`None`) the psyche-host-failure state on a
/// live perch (v0.8.1, REQ-HAZARD-LIVEHOST-BOOT-RACE) — the harness-reachable
/// mirror of the brain's invisible `LIVEHOST_PSYCHE_FAIL` stderr line.
///
/// - `Some(reason)` — set/overwrite the CURRENT-STATE stamp: `reason` is the
///   `spawn_psyche` error string, `ts` is now ([`crate::timefmt::now_rfc3339`]
///   — the same clock every other info.json string stamp uses), and `attempts`
///   is the prior `attempts + 1` (read inside the locked RMW so concurrent
///   retries can't lose a count), `1` on the first failure.
/// - `None` — clear it (a later successful host shows clean).
///
/// `status` is **never** touched — daemon liveness stays authoritative
/// (KH 2.5). Serialized via [`mutate_info`]; errors if the record is absent —
/// the daemon only stamps perches it hosts.
// [impl->REQ-HAZARD-LIVEHOST-BOOT-RACE]
pub fn set_psyche_host_error(perch_path: &Path, reason: Option<&str>) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| match reason {
        Some(r) => {
            let attempts = rec
                .psyche_host_error
                .as_ref()
                .map(|e| e.attempts.saturating_add(1))
                .unwrap_or(1);
            rec.psyche_host_error = Some(PsycheHostError {
                reason: r.to_string(),
                ts: crate::timefmt::now_rfc3339(),
                attempts,
            });
        }
        None => rec.psyche_host_error = None,
    })
}

/// Number of read attempts before giving up (torn-read retry).
const READ_ATTEMPTS: u32 = 5;


crates\spt-store\src\info.rs:100:    pub driven_by: Option<String>,
crates\spt-store\src\info.rs:109:    pub viewer_count: Option<u32>,
crates\spt-store\src\info.rs:192:            driven_by: None,
crates\spt-store\src\info.rs:193:            viewer_count: None,
crates\spt-store\src\info.rs:294:/// Stamp the read-only `viewer_count` (REQ-RCVIEW-1). The broker is the single
crates\spt-store\src\info.rs:298:pub fn set_viewer_count(perch_path: &Path, count: u32) -> std::io::Result<()> {
crates\spt-store\src\info.rs:300:        rec.viewer_count = if count == 0 { None } else { Some(count) }
crates\spt-store\src\info.rs:431:            driven_by: None,
crates\spt-store\src\info.rs:432:            viewer_count: None,
crates\spt-store\src\info.rs:505:    // the transient setters (last_active/rest/driven_by/viewer_count — each a
crates\spt-store\src\info.rs:533:                    let _ = set_viewer_count(&path, (n % 3) as u32);


80	    /// liveness and it is read authoritatively, **never** `is_process_alive(pid)`
81	    /// (KNOWN-HAZARDS 2.5). Absent ⇒ an external/interim perch whose liveness is
82	    /// the per-pid probe. The single liveness resolver
83	    /// ([`crate::liveness::is_perch_alive`]) is the only consumer.
84	    #[serde(skip_serializing_if = "Option::is_none")]
85	    pub status: Option<String>,
86	    /// Last-active wall-clock stamp (epoch ms), refreshed by the daemon on this
87	    /// endpoint's activity (pulse tick / input / bind). The recency signal the
88	    /// consent gate resolves "most-recently-active live session" against
89	    /// (REQ-UPD-4) — a v1 precursor to PresenceChannel dispatch. Absent ⇒ never
90	    /// stamped (an interim perch, or one with no activity yet); a resolver treats
91	    /// that as least-recent.
92	    #[serde(skip_serializing_if = "Option::is_none")]
93	    pub last_active_ms: Option<u64>,
94	    /// The node currently remote-driving this endpoint's session (hex node id),
95	    /// stamped by the daemon when it serves a net-attached surface and cleared
96	    /// when the viewport ends (REQ-REACH-1: detection is a daemon fact, not an
97	    /// agent guess). Absent ⇒ locally driven (or never net-attached). `default`
98	    /// so a pre-D5c record (no field) still parses.
99	    #[serde(default, skip_serializing_if = "Option::is_none")]
100	    pub driven_by: Option<String>,
101	    /// The number of read-only viewers currently attached to this endpoint's
102	    /// session under the W2.5 controller/viewer model (REQ-RCVIEW-1). The BROKER
103	    /// is the single writer (it owns the viewers map), stamped on every viewer
104	    /// add/remove/evict; the picker renders `controlled by <driven_by> (+N
105	    /// viewing)`. Absent / `0` ⇒ no viewers (or a pre-W2.5 record). Same
106	    /// best-effort/self-heal caveat as `driven_by`. `default` so older records
107	    /// still parse.
108	    #[serde(default, skip_serializing_if = "Option::is_none")]
109	    pub viewer_count: Option<u32>,
110	    /// The daemon-owned resting state (D9-2, REQ-INST-3): `"active"` /
111	    /// `"dormant"` / `"suspended"` — the explicit instance state machine's
112	    /// durable record (`spt_daemon::resting` owns the transitions; this field
113	    /// is only ever written through [`set_rest_state`]). Distinct from
114	    /// `status`: `status` is daemon **liveness** (online/offline, KH 2.5);
115	    /// `rest_state` is the *resting model* of a live instance. Absent ⇒ a
116	    /// pre-D9-2 / interim record (readers fall back to liveness-derived
117	    /// status). `default` so older records still parse.
118	    #[serde(default, skip_serializing_if = "Option::is_none")]
119	    pub rest_state: Option<String>,
120	    /// When the instance entered dormancy (epoch ms) — the anchor the opt-in
121	    /// `auto-suspend-after` threshold counts from (REQ-INST-3: counted from
122	    /// dormancy onset, never from last activity). Present iff `rest_state`
123	    /// is `"dormant"`; cleared on every other transition.
124	    #[serde(default, skip_serializing_if = "Option::is_none")]
125	    pub dormant_since_ms: Option<u64>,
126	    /// Per-endpoint `auto-suspend-after` override (ms) — the endpoint leg of
127	    /// the global → node → endpoint knob chain (REQ-INST-3). Absent ⇒ inherit
128	    /// the node knob (`daemon.json`); `0` ⇒ explicitly OFF for this endpoint
129	    /// even when the node enables it.
130	    #[serde(default, skip_serializing_if = "Option::is_none")]
131	    pub auto_suspend_after_ms: Option<u64>,
132	    /// The endpoint-authored resource-advertisement blurb (D9-4,
133	    /// REQ-INST-14): the agent's own "what I can serve" line, written via
134	    /// `spt resources set` ([`set_resources`]) and advertised into every
135	    /// visible subnet by the daemon's registry scan. Absent ⇒ the node-level
136	    /// `daemon.json` seed (if any) advertises instead — config seeds the
137	    /// default, the agent refines its own at runtime (CONTEXT §resource
138	    /// advertisement, both-authored).
139	    #[serde(default, skip_serializing_if = "Option::is_none")]
140	    pub resources: Option<String>,
141	    /// The endpoint's **immutable home subnet** (D9-5, REQ-INST-15 —
142	    /// ADR-0010): assigned at creation ([`crate::home::assign_home`]) or by
143	    /// first-join adoption ([`crate::home::adopt_for_unset`]), then never
144	    /// changed — **no setter exists**; `spt fork` is the cross-subnet move.
145	    /// Absent ⇒ local-only (unpaired at creation, awaiting first join) or a
146	    /// pre-D9-5 record. Creation seams must **carry this forward** on a
147	    /// re-bind (a revive rewrites `info.json`; home survives by contract).
148	    #[serde(default, skip_serializing_if = "Option::is_none")]
149	    pub home_subnet: Option<String>,
150	    /// The adapter chosen at creation (D9-5, REQ-INST-15 — the
151	    /// adapter-at-creation rule): recorded when a creation/bind path knows
152	    /// it, changed **only** via launch/resume-under-new (the bind paths are
153	    /// the sole writers; no standalone setter). Absent ⇒ created by an
154	    /// adapterless surface (`spt ready`) or a pre-D9-5 record.
155	    #[serde(default, skip_serializing_if = "Option::is_none")]
156	    pub adapter: Option<String>,
157	    /// The CURRENT psyche-host-failure state (v0.8.1,
158	    /// REQ-HAZARD-LIVEHOST-BOOT-RACE): present ⇒ the brain's live-host
159	    /// reconcile failed to spawn this perch's Psyche on its most recent
160	    /// attempt (the boot-race / missing-psyche-binary failure that was
161	    /// otherwise silent on the brain's stderr). Cleared (`None`) on a later
162	    /// successful host, so a recovered live agent shows clean. `default` so
163	    /// every pre-v0.8.1 record still parses (N-1-safe, additive). Set/cleared
164	    /// only through [`set_psyche_host_error`].
165	    #[serde(default, skip_serializing_if = "Option::is_none")]
166	    pub psyche_host_error: Option<PsycheHostError>,
167	    /// Whether this endpoint is **broker-PTY-controllable** (REQ-PICKER-1): a
168	    /// spt-hosted session the daemon spawned into a broker-owned PTY is `Some(true)`
169	    /// (attachable/drivable); a harness-hosted LiveAgent (bound via `api listen` —
170	    /// the harness owns the process, no broker PTY) is `Some(false)`. Stamped at the
171	    /// establish seam (carry-forward on revive). `None` ⇒ a pre-v0.10.0 record, an
172	    /// adapterless `ready`, or a non-live bind — the picker's status derivation
173	    /// type-gates so only a LiveAgent ever renders the amber "harness-only" state.
174	    /// `default` so every older record parses (N-1-safe, additive).
175	    #[serde(default, skip_serializing_if = "Option::is_none")]
176	    pub controllable: Option<bool>,
177	}
178
179	impl InfoJson {
180	    /// Construct a numeric-pid record.
181	    pub fn new(id: &str, started: &str, pid: u32, session_id: &str, state: &str) -> InfoJson {
182	        InfoJson {
183	            id: id.to_string(),
184	            started: started.to_string(),
185	            pid: PidValue::Numeric(pid),
186	            session_id: session_id.to_string(),
187	            state: state.to_string(),
188	            parent_pid: None,
189	            cwd: None,
190	            status: None,
191	            last_active_ms: None,
192	            driven_by: None,
193	            viewer_count: None,
194	            rest_state: None,
195	            dormant_since_ms: None,
196	            auto_suspend_after_ms: None,
197	            resources: None,
198	            home_subnet: None,
199	            adapter: None,

    /// liveness and it is read authoritatively, **never** `is_process_alive(pid)`
    /// (KNOWN-HAZARDS 2.5). Absent ⇒ an external/interim perch whose liveness is
    /// the per-pid probe. The single liveness resolver
    /// ([`crate::liveness::is_perch_alive`]) is the only consumer.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
    /// Last-active wall-clock stamp (epoch ms), refreshed by the daemon on this
    /// endpoint's activity (pulse tick / input / bind). The recency signal the
    /// consent gate resolves "most-recently-active live session" against
    /// (REQ-UPD-4) — a v1 precursor to PresenceChannel dispatch. Absent ⇒ never
    /// stamped (an interim perch, or one with no activity yet); a resolver treats
    /// that as least-recent.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_active_ms: Option<u64>,
    /// The node currently remote-driving this endpoint's session (hex node id),
    /// stamped by the daemon when it serves a net-attached surface and cleared
    /// when the viewport ends (REQ-REACH-1: detection is a daemon fact, not an
    /// agent guess). Absent ⇒ locally driven (or never net-attached). `default`
    /// so a pre-D5c record (no field) still parses.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub driven_by: Option<String>,
    /// The number of read-only viewers currently attached to this endpoint's
    /// session under the W2.5 controller/viewer model (REQ-RCVIEW-1). The BROKER
    /// is the single writer (it owns the viewers map), stamped on every viewer
    /// add/remove/evict; the picker renders `controlled by <driven_by> (+N
    /// viewing)`. Absent / `0` ⇒ no viewers (or a pre-W2.5 record). Same
    /// best-effort/self-heal caveat as `driven_by`. `default` so older records
    /// still parse.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub viewer_count: Option<u32>,
    /// The daemon-owned resting state (D9-2, REQ-INST-3): `"active"` /
    /// `"dormant"` / `"suspended"` — the explicit instance state machine's
    /// durable record (`spt_daemon::resting` owns the transitions; this field
    /// is only ever written through [`set_rest_state`]). Distinct from
    /// `status`: `status` is daemon **liveness** (online/offline, KH 2.5);
    /// `rest_state` is the *resting model* of a live instance. Absent ⇒ a
    /// pre-D9-2 / interim record (readers fall back to liveness-derived
    /// status). `default` so older records still parse.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rest_state: Option<String>,
    /// When the instance entered dormancy (epoch ms) — the anchor the opt-in
    /// `auto-suspend-after` threshold counts from (REQ-INST-3: counted from
    /// dormancy onset, never from last activity). Present iff `rest_state`
    /// is `"dormant"`; cleared on every other transition.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dormant_since_ms: Option<u64>,
    /// Per-endpoint `auto-suspend-after` override (ms) — the endpoint leg of
    /// the global → node → endpoint knob chain (REQ-INST-3). Absent ⇒ inherit
    /// the node knob (`daemon.json`); `0` ⇒ explicitly OFF for this endpoint
    /// even when the node enables it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub auto_suspend_after_ms: Option<u64>,
    /// The endpoint-authored resource-advertisement blurb (D9-4,
    /// REQ-INST-14): the agent's own "what I can serve" line, written via
    /// `spt resources set` ([`set_resources`]) and advertised into every
    /// visible subnet by the daemon's registry scan. Absent ⇒ the node-level
    /// `daemon.json` seed (if any) advertises instead — config seeds the
    /// default, the agent refines its own at runtime (CONTEXT §resource
    /// advertisement, both-authored).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resources: Option<String>,
    /// The endpoint's **immutable home subnet** (D9-5, REQ-INST-15 —
    /// ADR-0010): assigned at creation ([`crate::home::assign_home`]) or by
    /// first-join adoption ([`crate::home::adopt_for_unset`]), then never
    /// changed — **no setter exists**; `spt fork` is the cross-subnet move.
    /// Absent ⇒ local-only (unpaired at creation, awaiting first join) or a
    /// pre-D9-5 record. Creation seams must **carry this forward** on a
    /// re-bind (a revive rewrites `info.json`; home survives by contract).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub home_subnet: Option<String>,
    /// The adapter chosen at creation (D9-5, REQ-INST-15 — the
    /// adapter-at-creation rule): recorded when a creation/bind path knows
    /// it, changed **only** via launch/resume-under-new (the bind paths are
    /// the sole writers; no standalone setter). Absent ⇒ created by an
    /// adapterless surface (`spt ready`) or a pre-D9-5 record.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub adapter: Option<String>,
    /// The CURRENT psyche-host-failure state (v0.8.1,
    /// REQ-HAZARD-LIVEHOST-BOOT-RACE): present ⇒ the brain's live-host
    /// reconcile failed to spawn this perch's Psyche on its most recent
    /// attempt (the boot-race / missing-psyche-binary failure that was
    /// otherwise silent on the brain's stderr). Cleared (`None`) on a later
    /// successful host, so a recovered live agent shows clean. `default` so
    /// every pre-v0.8.1 record still parses (N-1-safe, additive). Set/cleared
    /// only through [`set_psyche_host_error`].
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub psyche_host_error: Option<PsycheHostError>,
    /// Whether this endpoint is **broker-PTY-controllable** (REQ-PICKER-1): a
    /// spt-hosted session the daemon spawned into a broker-owned PTY is `Some(true)`
    /// (attachable/drivable); a harness-hosted LiveAgent (bound via `api listen` —
    /// the harness owns the process, no broker PTY) is `Some(false)`. Stamped at the
    /// establish seam (carry-forward on revive). `None` ⇒ a pre-v0.10.0 record, an
    /// adapterless `ready`, or a non-live bind — the picker's status derivation
    /// type-gates so only a LiveAgent ever renders the amber "harness-only" state.
    /// `default` so every older record parses (N-1-safe, additive).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub controllable: Option<bool>,
}

impl InfoJson {
    /// Construct a numeric-pid record.
    pub fn new(id: &str, started: &str, pid: u32, session_id: &str, state: &str) -> InfoJson {
        InfoJson {
            id: id.to_string(),
            started: started.to_string(),
            pid: PidValue::Numeric(pid),
            session_id: session_id.to_string(),
            state: state.to_string(),
            parent_pid: None,
            cwd: None,
            status: None,
            last_active_ms: None,
            driven_by: None,
            viewer_count: None,
            rest_state: None,
            dormant_since_ms: None,
            auto_suspend_after_ms: None,
            resources: None,
            home_subnet: None,
            adapter: None,


180	    /// closure, so a blocking `PtyWrite` (the operator's keystroke into a
181	    /// not-draining ConPTY/forkpty) parked the lock forever → the single-threaded
182	    /// dispatch could open no attaches (`spt rc` → "brain IPC read deadline"), and
183	    /// every keystroke paid two fsyncs serialized (the stutter). Now: reserve the
184	    /// key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the
185	    /// effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable,
186	    /// mark applied). Crash-idempotency comes from the per-key reservation + the
187	    /// applied-set, not from holding the lock across the effect.
188	    ///
189	    /// **Ephemeral PtyWrite** ([`EffectKind::is_durable`] = false) writes NO
190	    /// journal lines and pays NO fsync — it is deduped in-memory only (a lost
191	    /// keystroke on a broker crash is retyped; PTY state is never rebuilt from
192	    /// keystroke replay). Durable kinds keep their fsync'd `PENDING`/`DONE` markers.
193	    ///
194	    /// The brain-crash guarantee is unchanged: the broker survives the brain, so
195	    /// the reserve→effect→finalize sequence runs to completion on the broker thread
196	    /// uninterrupted; a re-driven (replayed) effect hits the applied-set and dedups.
197	    /// Same-key concurrency does not occur in the model (a crashed brain is gone
198	    /// before its successor re-drives the same durable id), so the released window
199	    /// is only ever crossed by *different* keys — which is the entire point.
200	    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
201	    // [impl->REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]
202	    pub fn apply_once<F>(&self, key: EffectKey, kind: EffectKind, effect: F) -> io::Result<Outcome>
203	    where
204	        F: FnOnce() -> io::Result<()>,
205	    {
206	        let durable = kind.is_durable();
207	        // ── Phase 1: check + reserve under the lock (fsync PENDING for durable). ──
208	        {
209	            let mut g = self.inner.lock().expect("effect journal lock poisoned");
210	            if g.applied.contains(&key) {
211	                return Ok(Outcome::Deduped); // dedup-at-effect: already landed
212	            }
213	            if durable {
214	                write_line(
215	                    &mut g.file,
216	                    &format!("PENDING {} {} {}", key.0, key.1, kind.as_str()),
217	                )?;
218	            }
219	            g.pending.insert(key);
220	        }
221	        // ── Phase 2: run the real effect OFF the lock (a blocking PtyWrite can no
222	        //    longer freeze every other effect — the W1b wedge fix). A brain crash
223	        //    cannot interleave: this is the broker's thread, and it survives. ──
224	        let result = effect();
225	        // ── Phase 3: finalize under the lock. ──
226	        let mut g = self.inner.lock().expect("effect journal lock poisoned");
227	        match result {
228	            Ok(()) => {
229	                if durable {
230	                    write_line(&mut g.file, &format!("DONE {} {}", key.0, key.1))?;
231	                }
232	                g.pending.remove(&key);
233	                g.applied.insert(key);
234	                Ok(Outcome::Applied)
235	            }
236	            // The effect did not complete: leave the (durable) PENDING marker for
237	            // recovery surfacing and do NOT mark applied, so a re-drive re-attempts
238	            // it (no drop) — the pre-W1b error semantics.
239	            Err(e) => Err(e),
240	        }
241	    }
242
243	    /// Whether `key`'s effect has already been applied + recorded.
244	    pub fn is_applied(&self, key: EffectKey) -> bool {
245	        self.inner
246	            .lock()
247	            .expect("effect journal lock poisoned")
248	            .applied
249	            .contains(&key)
250	    }
251
252	    /// A snapshot of the applied-set (for introspection / tests). Unordered.
253	    pub fn applied_keys(&self) -> Vec<EffectKey> {
254	        self.inner
255	            .lock()
256	            .expect("effect journal lock poisoned")
257	            .applied
258	            .iter()
259	            .copied()
260	            .collect()
261	    }
262
263	    /// Count of distinct effects applied.
264	    pub fn applied_count(&self) -> usize {
265	        self.inner
266	            .lock()
267	            .expect("effect journal lock poisoned")
268	            .applied
269	            .len()

    /// closure, so a blocking `PtyWrite` (the operator's keystroke into a
    /// not-draining ConPTY/forkpty) parked the lock forever → the single-threaded
    /// dispatch could open no attaches (`spt rc` → "brain IPC read deadline"), and
    /// every keystroke paid two fsyncs serialized (the stutter). Now: reserve the
    /// key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the
    /// effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable,
    /// mark applied). Crash-idempotency comes from the per-key reservation + the
    /// applied-set, not from holding the lock across the effect.
    ///
    /// **Ephemeral PtyWrite** ([`EffectKind::is_durable`] = false) writes NO
    /// journal lines and pays NO fsync — it is deduped in-memory only (a lost
    /// keystroke on a broker crash is retyped; PTY state is never rebuilt from
    /// keystroke replay). Durable kinds keep their fsync'd `PENDING`/`DONE` markers.
    ///
    /// The brain-crash guarantee is unchanged: the broker survives the brain, so
    /// the reserve→effect→finalize sequence runs to completion on the broker thread
    /// uninterrupted; a re-driven (replayed) effect hits the applied-set and dedups.
    /// Same-key concurrency does not occur in the model (a crashed brain is gone
    /// before its successor re-drives the same durable id), so the released window
    /// is only ever crossed by *different* keys — which is the entire point.
    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
    // [impl->REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]
    pub fn apply_once<F>(&self, key: EffectKey, kind: EffectKind, effect: F) -> io::Result<Outcome>
    where
        F: FnOnce() -> io::Result<()>,
    {
        let durable = kind.is_durable();
        // ── Phase 1: check + reserve under the lock (fsync PENDING for durable). ──
        {
            let mut g = self.inner.lock().expect("effect journal lock poisoned");
            if g.applied.contains(&key) {
                return Ok(Outcome::Deduped); // dedup-at-effect: already landed
            }
            if durable {
                write_line(
                    &mut g.file,
                    &format!("PENDING {} {} {}", key.0, key.1, kind.as_str()),
                )?;
            }
            g.pending.insert(key);
        }
        // ── Phase 2: run the real effect OFF the lock (a blocking PtyWrite can no
        //    longer freeze every other effect — the W1b wedge fix). A brain crash
        //    cannot interleave: this is the broker's thread, and it survives. ──
        let result = effect();
        // ── Phase 3: finalize under the lock. ──
        let mut g = self.inner.lock().expect("effect journal lock poisoned");
        match result {
            Ok(()) => {
                if durable {
                    write_line(&mut g.file, &format!("DONE {} {}", key.0, key.1))?;
                }
                g.pending.remove(&key);
                g.applied.insert(key);
                Ok(Outcome::Applied)
            }
            // The effect did not complete: leave the (durable) PENDING marker for
            // recovery surfacing and do NOT mark applied, so a re-drive re-attempts
            // it (no drop) — the pre-W1b error semantics.
            Err(e) => Err(e),
        }
    }

    /// Whether `key`'s effect has already been applied + recorded.
    pub fn is_applied(&self, key: EffectKey) -> bool {
        self.inner
            .lock()
            .expect("effect journal lock poisoned")
            .applied
            .contains(&key)
    }

    /// A snapshot of the applied-set (for introspection / tests). Unordered.
    pub fn applied_keys(&self) -> Vec<EffectKey> {
        self.inner
            .lock()
            .expect("effect journal lock poisoned")
            .applied
            .iter()
            .copied()
            .collect()
    }

    /// Count of distinct effects applied.
    pub fn applied_count(&self) -> usize {
        self.inner
            .lock()
            .expect("effect journal lock poisoned")
            .applied
            .len()


Full picture acquired. Now implement. First — activate REQ in registry.