90	#[derive(Default)]
91	pub struct WakeSet {
92	    inner: Mutex<HashMap<(String, String), WatcherHandle>>,
93	}
94
95	struct WatcherHandle {
96	    stop: Arc<AtomicBool>,
97	    thread: JoinHandle<()>,
98	}
99
100	impl WakeSet {
101	    pub fn new() -> Self {
102	        Self::default()
103	    }
104
105	    /// How many watchers are live (finished threads pruned first).
106	    pub fn len(&self) -> usize {
107	        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
108	        map.retain(|_, h| !h.thread.is_finished());
109	        map.len()
110	    }
111
112	    pub fn is_empty(&self) -> bool {
113	        self.len() == 0
114	    }
115
116	    fn contains(&self, owner: &str, shell_id: &str) -> bool {
117	        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
118	        match map.get(&(owner.to_string(), shell_id.to_string())) {
119	            Some(h) if !h.thread.is_finished() => true,
120	            Some(_) => {
121	                map.remove(&(owner.to_string(), shell_id.to_string()));
122	                false
123	            }
124	            None => false,
125	        }
126	    }
127
128	    fn insert(&self, owner: &str, shell_id: &str, handle: WatcherHandle) {
129	        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
130	        map.insert((owner.to_string(), shell_id.to_string()), handle);
131	    }
132
133	    /// Signal one watcher to stop and kill its child (the perch pid file is
134	    /// the kill handle). The thread sees the kill as a non-opcode exit with
135	    /// its stop flag up and returns without counting a failure.
136	    fn stop_watcher(&self, owlery: &Path, owner: &str, shell_id: &str) {
137	        let handle = {
138	            let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
139	            map.remove(&(owner.to_string(), shell_id.to_string()))
140	        };
141	        if let Some(h) = handle {
142	            h.stop.store(true, Ordering::SeqCst);
143	            let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, owner, shell_id);
144	            kill_waker_at(&perch);
145	            let _ = h.thread.join();
146	        }
147	    }
148	}
149
150	/// Kill the recorded waker pid (if any) and retire the pid file — the
151	/// mutual-exclusivity enforcement point and the orphan cleanup
152	/// ([`crate::shellhost::launch_shell`] calls this before the binary rises;
153	/// the reconciler calls it before adopting an instance a dead daemon left).
154	// [impl->REQ-SHELL-2]
155	pub fn kill_waker_at(shell_perch: &Path) {
156	    let pid_file = shell_perch.join(WAKER_PID_FILE);
157	    if let Ok(s) = std::fs::read_to_string(&pid_file) {
158	        if let Ok(pid) = s.trim().parse::<u32>() {
159	            if pid != 0 && spt_store::proc::is_process_alive(pid) {
160	                crate::shellhost::kill_shell_pid(pid);
161	            }
162	        }
163	    }
164	    let _ = std::fs::remove_file(pid_file);

#[derive(Default)]
pub struct WakeSet {
    inner: Mutex<HashMap<(String, String), WatcherHandle>>,
}

struct WatcherHandle {
    stop: Arc<AtomicBool>,
    thread: JoinHandle<()>,
}

impl WakeSet {
    pub fn new() -> Self {
        Self::default()
    }

    /// How many watchers are live (finished threads pruned first).
    pub fn len(&self) -> usize {
        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
        map.retain(|_, h| !h.thread.is_finished());
        map.len()
    }

    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    fn contains(&self, owner: &str, shell_id: &str) -> bool {
        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
        match map.get(&(owner.to_string(), shell_id.to_string())) {
            Some(h) if !h.thread.is_finished() => true,
            Some(_) => {
                map.remove(&(owner.to_string(), shell_id.to_string()));
                false
            }
            None => false,
        }
    }

    fn insert(&self, owner: &str, shell_id: &str, handle: WatcherHandle) {
        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
        map.insert((owner.to_string(), shell_id.to_string()), handle);
    }

    /// Signal one watcher to stop and kill its child (the perch pid file is
    /// the kill handle). The thread sees the kill as a non-opcode exit with
    /// its stop flag up and returns without counting a failure.
    fn stop_watcher(&self, owlery: &Path, owner: &str, shell_id: &str) {
        let handle = {
            let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
            map.remove(&(owner.to_string(), shell_id.to_string()))
        };
        if let Some(h) = handle {
            h.stop.store(true, Ordering::SeqCst);
            let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, owner, shell_id);
            kill_waker_at(&perch);
            let _ = h.thread.join();
        }
    }
}

/// Kill the recorded waker pid (if any) and retire the pid file — the
/// mutual-exclusivity enforcement point and the orphan cleanup
/// ([`crate::shellhost::launch_shell`] calls this before the binary rises;
/// the reconciler calls it before adopting an instance a dead daemon left).
// [impl->REQ-SHELL-2]
pub fn kill_waker_at(shell_perch: &Path) {
    let pid_file = shell_perch.join(WAKER_PID_FILE);
    if let Ok(s) = std::fs::read_to_string(&pid_file) {
        if let Ok(pid) = s.trim().parse::<u32>() {
            if pid != 0 && spt_store::proc::is_process_alive(pid) {
                crate::shellhost::kill_shell_pid(pid);
            }
        }
    }
    let _ = std::fs::remove_file(pid_file);


WakeSet pattern clear. Find `spawn_wake_host` (the loop-thread + registered-loading structure) to mirror for the brainproc wiring.


<owl_messages>
<owl_message from="doyle" priority="high">clear — understanding is exact, nothing to add. Go write crate::livehost. On-perch for W0.1 evidence-land; I gate the impl vs the design (reconcile-diff completeness incl un-host + the real-brain W0.4 arc are what I check hardest). Shout on any edge mid-write.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


crates\spt-daemon\src\shellwake.rs:456:pub fn reconcile_once(
crates\spt-daemon\src\shellwake.rs:538:pub fn spawn_wake_host(stop: Arc<AtomicBool>) -> JoinHandle<()> {
crates\spt-daemon\src\shellwake.rs:546:            reconcile_once(&owlery, &registered, &adapters_dir, &set, &params);
crates\spt-daemon\src\shellwake.rs:878:        reconcile_once(owlery, &registered, &adapters_dir, &set, &params);
crates\spt-daemon\src\shellwake.rs:880:        reconcile_once(owlery, &registered, &adapters_dir, &set, &params);
crates\spt-daemon\src\shellwake.rs:904:        reconcile_once(owlery, &registered, &adapters_dir, &set, &params);
crates\spt-daemon\src\shellwake.rs:916:        reconcile_once(owlery, &registered, &adapters_dir, &set, &params);
crates\spt-daemon\src\shellwake.rs:962:        reconcile_once(owlery, &registered, &adapters_dir, &set, &params);


Grep

pub fn list\b|pub fn registered|pub fn list_registered|fn list_all|-> Vec<\(AdapterRecord


crates\spt-runtime\src\registry.rs:402:pub fn registered(adapters_dir: &Path) -> Vec<(AdapterRecord, Manifest)> {


Acknowledged doyle's "go" — proceeding to write. No reply needed (pure go-confirm). Reading the `spawn_wake_host` loop structure + `registered()` to finalize the brainproc wiring pattern.


<owl_messages>
<owl_message from="doyle" priority="high">complete + correct — stop-side-first set-diff, a-e covered, entry(b)/exit(e) distinct. Nothing left to gate on the design. Go heads-down; I will NOT interrupt the write. Next from me is the impl gate at your W0.1 evidence-land. Build.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


530	/// The reconcile cadence (ms): lifecycle flips land in CLI processes, so the
531	/// daemon's loop is the invariant holder — a flip is at most one tick stale.
532	pub const RECONCILE_INTERVAL_MS: u64 = 5_000;
533
534	/// Spawn the daemon's wake host: one thread sweeping [`reconcile_once`] at
535	/// boot and every [`RECONCILE_INTERVAL_MS`] until `stop`. The registered set
536	/// is re-read each sweep (adapter add/remove lands between ticks).
537	// [impl->REQ-SHELL-2]
538	pub fn spawn_wake_host(stop: Arc<AtomicBool>) -> JoinHandle<()> {
539	    std::thread::spawn(move || {
540	        let set = Arc::new(WakeSet::new());
541	        let params = WakeParams::default();
542	        while !stop.load(Ordering::SeqCst) {
543	            let owlery = spt_store::perch::owlery_dir();
544	            let adapters_dir = spt_store::perch::adapters_dir();
545	            let registered = spt_runtime::registry::registered(&adapters_dir);
546	            reconcile_once(&owlery, &registered, &adapters_dir, &set, &params);
547	            // Sleep in slices so a stop lands promptly.
548	            let mut left = RECONCILE_INTERVAL_MS;
549	            while left > 0 && !stop.load(Ordering::SeqCst) {
550	                let step = left.min(100);
551	                std::thread::sleep(Duration::from_millis(step));
552	                left -= step;
553	            }
554	        }
555	    })
556	}
557
558	#[cfg(test)]
559	mod tests {
560	    use super::*;
561	    use spt_store::shellinfo::{spawn_record, SHELL_STATUS_ONLINE};
562
563	    fn fast_params() -> WakeParams {
564	        WakeParams {

/// The reconcile cadence (ms): lifecycle flips land in CLI processes, so the
/// daemon's loop is the invariant holder — a flip is at most one tick stale.
pub const RECONCILE_INTERVAL_MS: u64 = 5_000;

/// Spawn the daemon's wake host: one thread sweeping [`reconcile_once`] at
/// boot and every [`RECONCILE_INTERVAL_MS`] until `stop`. The registered set
/// is re-read each sweep (adapter add/remove lands between ticks).
// [impl->REQ-SHELL-2]
pub fn spawn_wake_host(stop: Arc<AtomicBool>) -> JoinHandle<()> {
    std::thread::spawn(move || {
        let set = Arc::new(WakeSet::new());
        let params = WakeParams::default();
        while !stop.load(Ordering::SeqCst) {
            let owlery = spt_store::perch::owlery_dir();
            let adapters_dir = spt_store::perch::adapters_dir();
            let registered = spt_runtime::registry::registered(&adapters_dir);
            reconcile_once(&owlery, &registered, &adapters_dir, &set, &params);
            // Sleep in slices so a stop lands promptly.
            let mut left = RECONCILE_INTERVAL_MS;
            while left > 0 && !stop.load(Ordering::SeqCst) {
                let step = left.min(100);
                std::thread::sleep(Duration::from_millis(step));
                left -= step;
            }
        }
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use spt_store::shellinfo::{spawn_record, SHELL_STATUS_ONLINE};

    fn fast_params() -> WakeParams {
        WakeParams {


395	}
396
397	/// The **active** registered set with manifests resolved — the single source
398	/// for creation-time adapter selection, shell discovery, and the R-UPD-5
399	/// ripple. A pointer-mode record whose source tree has vanished is skipped
400	/// (tolerant), not fatal.
401	// [impl->REQ-INSTALL-4]
402	pub fn registered(adapters_dir: &Path) -> Vec<(AdapterRecord, Manifest)> {
403	    all_records(adapters_dir)
404	        .into_iter()
405	        .filter(|r| r.active)
406	        .filter_map(|r| load_manifest(adapters_dir, &r).ok().map(|m| (r, m)))
407	        .collect()
408	}
409
410	// ───────────────────────── Local profiles (M9 / REQ-MANIFEST-2) ─────────────
411	//
412	// A **local** profile is a node-local, user-authored sparse overlay registered
413	// *beside* the adapter at `adapters/<adapter>/profiles/<profile>.toml`. Because
414	// `register` never clears that dir, locals **survive adapter re-registration**
415	// (the manifest copy is adapter-owned and overwritten by updates; the user's
416	// customization is not). Locals may not shadow a shipped profile's name (refused
417	// at create) and are floored tighten-only at create. CLI verbs (`create-profile`,
418	// `delete-profile`, `set-string`) operate on locals only.
419
420	/// `adapters/<adapter>/profiles/`.
421	fn local_profiles_dir(adapters_dir: &Path, adapter: &str) -> PathBuf {
422	    adapters_dir.join(adapter).join("profiles")
423	}
424

}

/// The **active** registered set with manifests resolved — the single source
/// for creation-time adapter selection, shell discovery, and the R-UPD-5
/// ripple. A pointer-mode record whose source tree has vanished is skipped
/// (tolerant), not fatal.
// [impl->REQ-INSTALL-4]
pub fn registered(adapters_dir: &Path) -> Vec<(AdapterRecord, Manifest)> {
    all_records(adapters_dir)
        .into_iter()
        .filter(|r| r.active)
        .filter_map(|r| load_manifest(adapters_dir, &r).ok().map(|m| (r, m)))
        .collect()
}

// ───────────────────────── Local profiles (M9 / REQ-MANIFEST-2) ─────────────
//
// A **local** profile is a node-local, user-authored sparse overlay registered
// *beside* the adapter at `adapters/<adapter>/profiles/<profile>.toml`. Because
// `register` never clears that dir, locals **survive adapter re-registration**
// (the manifest copy is adapter-owned and overwritten by updates; the user's
// customization is not). Locals may not shadow a shipped profile's name (refused
// at create) and are floored tighten-only at create. CLI verbs (`create-profile`,
// `delete-profile`, `set-string`) operate on locals only.

/// `adapters/<adapter>/profiles/`.
fn local_profiles_dir(adapters_dir: &Path, adapter: &str) -> PathBuf {
    adapters_dir.join(adapter).join("profiles")
}


doyle heads-down till evidence-land. Verifying exact signatures before writing (avoid compile churn): `read_info`, `resolve_option_in`, liveness/state constants, `StartReason`.


pub fn read_info|STATUS_ONLINE|STATUS_OFFLINE|live_agent|STATE_LIVE|pub fn resolve_option_in|pub enum StartReason|EXIT_|pub const STATE


crates\spt-store\src\home.rs:273:        let mut rec = InfoJson::new("ling", "t", 1, "sid", "live_agent");
crates\spt-store\src\home.rs:284:        let mut fresh = InfoJson::new("oak", "t", 1, "sid", "live_agent");
crates\spt-store\src\home.rs:299:        let mut revived = InfoJson::new("ling", "t2", 2, "sid-2", "live_agent");
crates\spt-store\src\info.rs:284:pub fn read_info(perch_path: &Path) -> Option<InfoJson> {
crates\spt-store\src\info.rs:330:        let mut info = InfoJson::new("doyle", "2026-05-31T00:00:00Z", 4242, "sid-1", "live_agent");
crates\spt-store\src\info.rs:343:        assert!(raw.contains("\"state\":\"live_agent\""));
crates\spt-store\src\info.rs:386:            &InfoJson::new("doyle", "t", 4242, "sid", "live_agent"),
crates\spt-store\src\info.rs:420:            &InfoJson::new("doyle", "t", 4242, "sid", "live_agent"),
crates\spt-store\src\info.rs:449:            &InfoJson::new("ling", "t", 4242, "sid", "live_agent"),
crates\spt-store\src\info.rs:482:            r#"{"id":"ling","started":"t","pid":1,"session_id":"sid","state":"live_agent"}"#,
crates\spt-store\src\info.rs:498:            &InfoJson::new("ling", "t", 4242, "sid", "live_agent"),
crates\spt-store\src\info.rs:538:            &InfoJson::new("ling", "t", 4242, "sid", "live_agent"),
crates\spt-store\src\info.rs:568:            r#"{"id":"ling","started":"t","pid":1,"session_id":"sid","state":"live_agent"}"#,
crates\spt-store\src\info.rs:606:        write_info(&path, &InfoJson::new("doyle", "t", 1, "sid", "live_agent")).unwrap();
crates\spt-store\src\info.rs:616:                let mut info = InfoJson::new("doyle", "t", n, "sid", "live_agent");
crates\spt-store\src\liveness.rs:37:pub const STATUS_ONLINE: &str = "online";
crates\spt-store\src\liveness.rs:39:pub const STATUS_OFFLINE: &str = "offline";
crates\spt-store\src\liveness.rs:84:                return status == STATUS_ONLINE; // daemon-authoritative; never pid
crates\spt-store\src\liveness.rs:106:        Some(status) => status == STATUS_ONLINE, // daemon-hosted: authoritative
crates\spt-store\src\liveness.rs:136:        set_status(d.path(), STATUS_ONLINE).unwrap();
crates\spt-store\src\liveness.rs:153:        set_status(d.path(), STATUS_OFFLINE).unwrap();
crates\spt-store\src\liveness.rs:223:        set_status(&off, STATUS_OFFLINE).unwrap();
crates\spt-store\src\liveness.rs:233:        set_status(&on, STATUS_ONLINE).unwrap();
crates\spt-store\src\registry.rs:236:        use crate::liveness::STATUS_OFFLINE;
crates\spt-store\src\registry.rs:251:        set_status(&hosted, STATUS_OFFLINE).unwrap();
crates\spt-store\src\shellinfo.rs:31:pub const SHELL_STATUS_ONLINE: &str = "online";
crates\spt-store\src\shellinfo.rs:34:pub const SHELL_STATUS_OFFLINE: &str = "offline";
crates\spt-store\src\shellinfo.rs:51:    /// [`SHELL_STATUS_ONLINE`] | [`SHELL_STATUS_OFFLINE`].
crates\spt-store\src\shellinfo.rs:66:            status: SHELL_STATUS_OFFLINE.to_string(),
crates\spt-store\src\shellinfo.rs:413:        assert_eq!(info.status, SHELL_STATUS_OFFLINE);


crates\spt-runtime\src\registry.rs:652:pub fn resolve_option_in(
crates\spt-runtime\src\registry.rs-653-    registered: &[(AdapterRecord, Manifest)],
crates\spt-runtime\src\registry.rs-654-    adapters_dir: &Path,
crates\spt-runtime\src\registry.rs-655-    option: &str,
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\registry.rs-656-) -> Result<Manifest, RegistryError> {
crates\spt-runtime\src\registry.rs-657-    let (adapter, profile) = profile::split_option(option);
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\registry.rs-658-    let parent = registered


crates\spt-daemon\src\brainproc.rs:71:/// reason a *future* broker adds that this brain predates — defaults to `Cold`,
crates\spt-daemon\src\brainproc.rs:75:pub enum StartReason {
crates\spt-daemon\src\brainproc.rs:77:    Cold,
crates\spt-daemon\src\brainproc.rs:79:    Crash,
crates\spt-daemon\src\brainproc.rs:81:    Update,
crates\spt-daemon\src\brainproc.rs:88:            StartReason::Cold => "cold",
crates\spt-daemon\src\brainproc.rs:89:            StartReason::Crash => "crash",
crates\spt-daemon\src\brainproc.rs:90:            StartReason::Update => "update",
crates\spt-daemon\src\brainproc.rs:95:    /// newer broker's reason an older brain predates) defaults to `Cold` rather
crates\spt-daemon\src\brainproc.rs:99:            "update" => StartReason::Update,
crates\spt-daemon\src\brainproc.rs:100:            "crash" => StartReason::Crash,
crates\spt-daemon\src\brainproc.rs:101:            _ => StartReason::Cold,
crates\spt-daemon\src\brainproc.rs:545:/// pre-ready crash respawn (`reason=Crash`) and a reboot-mid-trial cold spawn
crates\spt-daemon\src\brainproc.rs:546:/// (`reason=Cold`) are *both* trials.
crates\spt-daemon\src\brainproc.rs:642:    // authoritative source, not the outgoing brain. The first spawn is `Cold`;

[Showing results with pagination = limit: 15]


All signatures confirmed (`read_info→Option`, `STATUS_ONLINE/OFFLINE`, `resolve_option_in→Result<Manifest>`, `StartReason{Cold,Crash,Update}`, state="live_agent"). Before writing the units I need the minimal live-capable manifest + psyche no-op + test-home helper. Read daemon_e2e's manifest setup + find `with_home`.


75	#[cfg(windows)]
76	const NOOP_CMD: &str = "cmd /C rem";
77	#[cfg(unix)]
78	const NOOP_CMD: &str = "true";
79
80	// A stdin→stdout passthrough so the echo-commune summarizer writes a real drop
81	// file — lets the signoff assert the echo (3.3) actually ran before teardown.
82	#[cfg(windows)]
83	const PASSTHROUGH: &str = "sort";
84	#[cfg(unix)]
85	const PASSTHROUGH: &str = "cat";
86
87	/// A live-capable manifest declaring a Psyche, a watched commune/signoff dir, and
88	/// an echo-commune summarizer (so `fire_echo` writes a real commune on signoff).
89	fn live_manifest(drops: &std::path::Path) -> Manifest {
90	    let dir = drops.to_string_lossy().replace('\\', "/");
91	    let toml = format!(
92	        "[adapter]\nname=\"mock\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
93	         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
94	         [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n\n\
95	         [session.echo_commune]\ncommand='{PASSTHROUGH}'\n"
96	    );
97	    Manifest::from_toml_str(&toml).unwrap()
98	}
99
100	fn cold_retry(name: &str, now_ms: u64) -> Brain {
101	    for _ in 0..300 {
102	        if let Ok(b) = Brain::cold_start(name, now_ms) {
103	            return b;
104	        }
105	        thread::sleep(Duration::from_millis(10));
106	    }
107	    panic!("brain cold-start could not connect");
108	}
109
110	fn handoff_retry(name: &str, prior: BrainState, now_ms: u64) -> Brain {
111	    for _ in 0..300 {
112	        if let Ok(b) = Brain::handoff(name, prior.clone(), now_ms) {
113	            return b;
114	        }
115	        thread::sleep(Duration::from_millis(10));
116	    }
117	    panic!("brain handoff could not connect");
118	}
119
120	#[test]
121	fn daemon_hosts_lifecycle_and_survives_brain_restart() {
122	    // Isolate all on-disk perch state (the Self perch, the nested Psyche perch,
123	    // the commune drops) to a temp home.
124	    let home = tempfile::tempdir().expect("tempdir");
125	    std::env::set_var("SPT_HOME", home.path());
126
127	    let id = "doyle";
128	    let drops = tempfile::tempdir().expect("drops dir");
129	    let manifest = live_manifest(drops.path());
130	    // A short pulse keeps the test quick; proves the period is config-driven.
131	    let cfg = DaemonConfig {
132	        pulse_period: Duration::from_millis(15),
133	        crash_grace: Duration::from_millis(10),
134	        ..DaemonConfig::default()
135	    };
136	    let host = BrainLifecycle::with_config(&manifest, id, cfg).expect("live host");
137
138	    // ── 1. spawn: the broker hosts a real PTY child; a brain drives it ──────────
139	    let broker_name = unique("broker");
140	    let broker = Broker::bind(&broker_name).expect("bind broker");
141	    {
142	        let broker = Arc::clone(&broker);
143	        thread::spawn(move || {
144	            let _ = broker.serve();
145	        });
146	    }
147	    let mut now = 1_000_000u64;
148	    let mut brain = cold_retry(&broker_name, now);
149	    let sid = brain.spawn_session(echo_req()).expect("spawn echo child");
150	    let child_pid = broker.session_pid(sid);
151	    assert_eq!(brain.generation(), 0, "cold start is generation 0");
152
153	    // The session is live: a marker round-trips through the real PTY.
154	    let mut buf = Vec::<u8>::new();
155	    brain.send_input(b"HELLO_DAEMON\r").expect("inject");
156	    brain
157	        .read_output_until(b"HELLO_DAEMON", &mut buf)
158	        .expect("marker round-trips");
159
160	    // ── 2. Psyche-loop: the daemon hosts the Psyche; liveness is the status ─────
161	    use spt_store::info::{read_info, write_info, PidValue};
162	    use spt_store::liveness::{is_perch_alive, STATUS_ONLINE};
163
164	    let psyche = host
165	        .spawn_psyche(Some("sid-1"), Some("pulse"))
166	        .expect("spawn psyche");
167	    assert_eq!(
168	        read_info(&psyche.perch_path)
169	            .expect("psyche info")

#[cfg(windows)]
const NOOP_CMD: &str = "cmd /C rem";
#[cfg(unix)]
const NOOP_CMD: &str = "true";

// A stdin→stdout passthrough so the echo-commune summarizer writes a real drop
// file — lets the signoff assert the echo (3.3) actually ran before teardown.
#[cfg(windows)]
const PASSTHROUGH: &str = "sort";
#[cfg(unix)]
const PASSTHROUGH: &str = "cat";

/// A live-capable manifest declaring a Psyche, a watched commune/signoff dir, and
/// an echo-commune summarizer (so `fire_echo` writes a real commune on signoff).
fn live_manifest(drops: &std::path::Path) -> Manifest {
    let dir = drops.to_string_lossy().replace('\\', "/");
    let toml = format!(
        "[adapter]\nname=\"mock\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
         [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n\n\
         [session.echo_commune]\ncommand='{PASSTHROUGH}'\n"
    );
    Manifest::from_toml_str(&toml).unwrap()
}

fn cold_retry(name: &str, now_ms: u64) -> Brain {
    for _ in 0..300 {
        if let Ok(b) = Brain::cold_start(name, now_ms) {
            return b;
        }
        thread::sleep(Duration::from_millis(10));
    }
    panic!("brain cold-start could not connect");
}

fn handoff_retry(name: &str, prior: BrainState, now_ms: u64) -> Brain {
    for _ in 0..300 {
        if let Ok(b) = Brain::handoff(name, prior.clone(), now_ms) {
            return b;
        }
        thread::sleep(Duration::from_millis(10));
    }
    panic!("brain handoff could not connect");
}

#[test]
fn daemon_hosts_lifecycle_and_survives_brain_restart() {
    // Isolate all on-disk perch state (the Self perch, the nested Psyche perch,
    // the commune drops) to a temp home.
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    let id = "doyle";
    let drops = tempfile::tempdir().expect("drops dir");
    let manifest = live_manifest(drops.path());
    // A short pulse keeps the test quick; proves the period is config-driven.
    let cfg = DaemonConfig {
        pulse_period: Duration::from_millis(15),
        crash_grace: Duration::from_millis(10),
        ..DaemonConfig::default()
    };
    let host = BrainLifecycle::with_config(&manifest, id, cfg).expect("live host");

    // ── 1. spawn: the broker hosts a real PTY child; a brain drives it ──────────
    let broker_name = unique("broker");
    let broker = Broker::bind(&broker_name).expect("bind broker");
    {
        let broker = Arc::clone(&broker);
        thread::spawn(move || {
            let _ = broker.serve();
        });
    }
    let mut now = 1_000_000u64;
    let mut brain = cold_retry(&broker_name, now);
    let sid = brain.spawn_session(echo_req()).expect("spawn echo child");
    let child_pid = broker.session_pid(sid);
    assert_eq!(brain.generation(), 0, "cold start is generation 0");

    // The session is live: a marker round-trips through the real PTY.
    let mut buf = Vec::<u8>::new();
    brain.send_input(b"HELLO_DAEMON\r").expect("inject");
    brain
        .read_output_until(b"HELLO_DAEMON", &mut buf)
        .expect("marker round-trips");

    // ── 2. Psyche-loop: the daemon hosts the Psyche; liveness is the status ─────
    use spt_store::info::{read_info, write_info, PidValue};
    use spt_store::liveness::{is_perch_alive, STATUS_ONLINE};

    let psyche = host
        .spawn_psyche(Some("sid-1"), Some("pulse"))
        .expect("spawn psyche");
    assert_eq!(
        read_info(&psyche.perch_path)
            .expect("psyche info")


crates\spt-daemon\src\test_home.rs:24:pub(crate) fn with_home<F: FnOnce(&Path)>(body: F) {


Have the live-manifest + NOOP pattern + `with_home`. Decision: thread `DaemonConfig` through `reconcile_once`/`host_one` (use `with_config`, not `new_opt`) so units inject a short pulse — cleaner test seam. Final reads: `with_home` + `DaemonConfig` (Clone/load), then write.


1	//! Shared `SPT_HOME` isolation for spt-daemon unit tests (mirrors the spt-live
2	//! `test_support` discipline).
3	//!
4	//! Every `SPT_HOME`-touching test in this binary races on the single global env
5	//! var, so they all serialize on one process-wide `Mutex`.
6
7	use std::path::Path;
8	use std::sync::{Mutex, MutexGuard};
9
10	static ENV_LOCK: Mutex<()> = Mutex::new(());
11
12	/// Acquire the process-global env-mutation lock directly. For a test that must
13	/// mutate `SPT_HOME` (or `HOME`/`USER`/…) in **set-AND-unset** states rather than
14	/// via a fresh tempdir — so it cannot use [`with_home`] — yet must still
15	/// serialize against every `with_home` test (which resolve the ambient env mid
16	/// body, e.g. store loads). Hold the returned guard across the whole mutation
17	/// window. Poison-tolerant (a panicking test must not wedge the rest).
18	pub(crate) fn env_lock() -> MutexGuard<'static, ()> {
19	    ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner())
20	}
21
22	/// Run `body` with `SPT_HOME` pointed at a fresh temp dir, serialized against
23	/// every other `SPT_HOME` test in this binary and restored afterward.
24	pub(crate) fn with_home<F: FnOnce(&Path)>(body: F) {
25	    let _g = env_lock();
26	    let prev = std::env::var("SPT_HOME").ok();
27	    let tmp = tempfile::tempdir().unwrap();
28	    std::env::set_var("SPT_HOME", tmp.path());
29	    body(tmp.path());
30	    match prev {
31	        Some(v) => std::env::set_var("SPT_HOME", v),
32	        None => std::env::remove_var("SPT_HOME"),
33	    }
34	}
35

//! Shared `SPT_HOME` isolation for spt-daemon unit tests (mirrors the spt-live
//! `test_support` discipline).
//!
//! Every `SPT_HOME`-touching test in this binary races on the single global env
//! var, so they all serialize on one process-wide `Mutex`.

use std::path::Path;
use std::sync::{Mutex, MutexGuard};

static ENV_LOCK: Mutex<()> = Mutex::new(());

/// Acquire the process-global env-mutation lock directly. For a test that must
/// mutate `SPT_HOME` (or `HOME`/`USER`/…) in **set-AND-unset** states rather than
/// via a fresh tempdir — so it cannot use [`with_home`] — yet must still
/// serialize against every `with_home` test (which resolve the ambient env mid
/// body, e.g. store loads). Hold the returned guard across the whole mutation
/// window. Poison-tolerant (a panicking test must not wedge the rest).
pub(crate) fn env_lock() -> MutexGuard<'static, ()> {
    ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner())
}

/// Run `body` with `SPT_HOME` pointed at a fresh temp dir, serialized against
/// every other `SPT_HOME` test in this binary and restored afterward.
pub(crate) fn with_home<F: FnOnce(&Path)>(body: F) {
    let _g = env_lock();
    let prev = std::env::var("SPT_HOME").ok();
    let tmp = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", tmp.path());
    body(tmp.path());
    match prev {
        Some(v) => std::env::set_var("SPT_HOME", v),
        None => std::env::remove_var("SPT_HOME"),
    }
}


pub struct DaemonConfig|derive|pub fn load|pulse_period|impl Default for DaemonConfig


crates\spt-daemon\src\config.rs:58:#[derive(Debug, Clone, Copy, PartialEq, Eq)]
crates\spt-daemon\src\config.rs:69:#[derive(Debug, Clone, Default, Serialize, Deserialize)]
crates\spt-daemon\src\config.rs:72:    pulse_period_ms: Option<u64>,
crates\spt-daemon\src\config.rs:102:/// from `pulse_period`; the drop-ingest guard reads `protection_window_ms`.
crates\spt-daemon\src\config.rs:104:#[derive(Debug, Clone, PartialEq, Eq)]
crates\spt-daemon\src\config.rs:105:pub struct DaemonConfig {
crates\spt-daemon\src\config.rs:107:    pub pulse_period: Duration,
crates\spt-daemon\src\config.rs:159:impl Default for DaemonConfig {
crates\spt-daemon\src\config.rs:162:            pulse_period: DEFAULT_PULSE_PERIOD,
crates\spt-daemon\src\config.rs:188:    pub fn load() -> Self {
crates\spt-daemon\src\config.rs:195:    pub fn load_from(path: &Path) -> Self {
crates\spt-daemon\src\config.rs:209:            pulse_period: file
crates\spt-daemon\src\config.rs:210:                .pulse_period_ms
crates\spt-daemon\src\config.rs:212:                .unwrap_or(d.pulse_period),
crates\spt-daemon\src\config.rs:254:            pulse_period_ms: Some(self.pulse_period.as_millis() as u64),
crates\spt-daemon\src\config.rs:290:        assert_eq!(cfg.pulse_period, DEFAULT_PULSE_PERIOD);
crates\spt-daemon\src\config.rs:294:    fn pulse_period_is_config_driven() {
crates\spt-daemon\src\config.rs:297:        std::fs::write(&path, r#"{"pulse_period_ms": 250}"#).unwrap();
crates\spt-daemon\src\config.rs:300:        assert_eq!(cfg.pulse_period, Duration::from_millis(250));
crates\spt-daemon\src\config.rs:302:            cfg.pulse_period, DEFAULT_PULSE_PERIOD,

[Showing results with pagination = limit: 20]


`DaemonConfig: Clone` ✓, `with_home` ✓. Confirm `StartReason` derives Copy (the reconcile loop reuses `reason` each sweep) + lib.rs module list, then write.


68	/// reliable than a brain→brain frame, which retires in D4).
69	///
70	/// Parsing is **lenient** (KH-2.3 forward-compat): an unrecognized value — a
71	/// reason a *future* broker adds that this brain predates — defaults to `Cold`,
72	/// never a parse failure that would brick the brain before it rehydrates.
73	// [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]
74	#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75	pub enum StartReason {
76	    /// The broker's first brain spawn this broker lifetime.
77	    Cold,
78	    /// A supervised respawn after an unexpected brain exit.
79	    Crash,
80	    /// A respawn the broker itself initiated for a binary swap (wired in D3-3).
81	    Update,
82	}
83
84	impl StartReason {
85	    /// The argv token the broker passes to `spt daemon brain --start-reason`.

/// reliable than a brain→brain frame, which retires in D4).
///
/// Parsing is **lenient** (KH-2.3 forward-compat): an unrecognized value — a
/// reason a *future* broker adds that this brain predates — defaults to `Cold`,
/// never a parse failure that would brick the brain before it rehydrates.
// [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StartReason {
    /// The broker's first brain spawn this broker lifetime.
    Cold,
    /// A supervised respawn after an unexpected brain exit.
    Crash,
    /// A respawn the broker itself initiated for a binary swap (wired in D3-3).
    Update,
}

impl StartReason {
    /// The argv token the broker passes to `spt daemon brain --start-reason`.


^mod |^pub mod |pub use.*lifecycle|pub use.*shellwake|pub use.*BrainLifecycle


crates\spt-daemon\src\lib.rs:106:pub mod access;
crates\spt-daemon\src\lib.rs:107:pub mod adapter_update;
crates\spt-daemon\src\lib.rs:108:pub mod applyhost;
crates\spt-daemon\src\lib.rs:109:pub mod attach;
crates\spt-daemon\src\lib.rs:110:pub mod brain;
crates\spt-daemon\src\lib.rs:111:pub mod brainproc;
crates\spt-daemon\src\lib.rs:112:pub mod broker;
crates\spt-daemon\src\lib.rs:113:pub mod codec;
crates\spt-daemon\src\lib.rs:114:pub mod config;
crates\spt-daemon\src\lib.rs:115:pub mod consent;
crates\spt-daemon\src\lib.rs:116:pub mod daemon;
crates\spt-daemon\src\lib.rs:117:pub mod deadline;
crates\spt-daemon\src\lib.rs:118:pub mod deelevate;
crates\spt-daemon\src\lib.rs:119:pub mod digest;
crates\spt-daemon\src\lib.rs:120:pub mod digesthub;
crates\spt-daemon\src\lib.rs:121:pub mod dispatch;
crates\spt-daemon\src\lib.rs:122:pub mod effect;
crates\spt-daemon\src\lib.rs:123:pub mod endpoint;
crates\spt-daemon\src\lib.rs:124:pub mod frame;
crates\spt-daemon\src\lib.rs:125:pub mod grants;
crates\spt-daemon\src\lib.rs:126:pub mod harnesshost;
crates\spt-daemon\src\lib.rs:127:pub mod lifecycle;
crates\spt-daemon\src\lib.rs:128:pub mod linkhost;
crates\spt-daemon\src\lib.rs:129:pub mod machineid;
crates\spt-daemon\src\lib.rs:130:pub mod msg;
crates\spt-daemon\src\lib.rs:131:pub mod nethost;
crates\spt-daemon\src\lib.rs:132:pub mod notif;
crates\spt-daemon\src\lib.rs:133:pub mod notifsync;
crates\spt-daemon\src\lib.rs:134:pub mod pairhost;
crates\spt-daemon\src\lib.rs:135:pub mod presence;
crates\spt-daemon\src\lib.rs:136:pub mod propagate;
crates\spt-daemon\src\lib.rs:137:pub mod psyrelay;
crates\spt-daemon\src\lib.rs:138:pub mod pump;
crates\spt-daemon\src\lib.rs:139:pub mod reconcile;
crates\spt-daemon\src\lib.rs:140:pub mod registryhost;
crates\spt-daemon\src\lib.rs:141:pub mod relay;
crates\spt-daemon\src\lib.rs:142:pub mod relcache;
crates\spt-daemon\src\lib.rs:143:pub mod release;
crates\spt-daemon\src\lib.rs:144:pub mod rollback_compat;
crates\spt-daemon\src\lib.rs:145:pub mod resthost;
crates\spt-daemon\src\lib.rs:146:pub mod resting;
crates\spt-daemon\src\lib.rs:147:pub mod seedmap;
crates\spt-daemon\src\lib.rs:148:pub mod seedproofx;
crates\spt-daemon\src\lib.rs:149:pub mod serveprobe;
crates\spt-daemon\src\lib.rs:150:pub mod service;
crates\spt-daemon\src\lib.rs:151:pub mod shellchan;
crates\spt-daemon\src\lib.rs:152:pub mod shelldisc;
crates\spt-daemon\src\lib.rs:153:pub mod shellhost;
crates\spt-daemon\src\lib.rs:154:pub mod shellwake;
crates\spt-daemon\src\lib.rs:155:pub mod sync;
crates\spt-daemon\src\lib.rs:156:pub mod transport;
crates\spt-daemon\src\lib.rs:157:pub mod update;
crates\spt-daemon\src\lib.rs:158:pub mod wan;
crates\spt-daemon\src\lib.rs:159:pub mod xfer;
crates\spt-daemon\src\lib.rs:162:mod test_home;
crates\spt-daemon\src\lib.rs:195:pub use lifecycle::{BrainLifecycle, TickReport};


All confirmed (`StartReason: Copy`, `set_status` in `info`, `with_home` crate-visible). Writing `livehost.rs` — reconcile set-diff + LiveSet + host_one + spawn_live_host + units a–e (pulse-liveness proven via `last_active_ms` stamping).


Write

//! The brain's live-agent Psyche host (M11-W0.1, REQ-DAEMON-1) — re-host the
//! daemon-hosted LiveAgent lifecycle from disk and reconcile the host-set.
//!
//! This is the production caller `run_pulse_loop` lacked: the brain instantiates
//! a [`BrainLifecycle`] per hosted **live** endpoint and drives its config-paced
//! pulse loop on a supervised thread. It is the **harness-endpoint mirror** of
//! [`crate::shellwake::reconcile_once`] (which re-hosts offline shells from
//! disk): same shape — enumerate Self perches, resolve each manifest from its
//! `info.json` `adapter` option, hold a stop flag per supervised thread, and run
//! one bounded reconcile sweep at boot and every tick. Where the shell waker
//! hosts **offline** shells, this hosts **online** live endpoints — `status` is
//! daemon-authoritative liveness (CONTEXT:194 / KH 2.5), so re-hosting the
//! online ones re-hosts exactly what was live before a brain (re)start, and
//! never resurrects a gracefully-departed (offline) agent.
//!
//! **Online-only is staged behind the interim.** The interim `spt api listen`
//! process still first-hosts its live agents today; those perches are
//! `status`-ABSENT (not "online"), so this scan ignores them — no double-host in
//! the W0.1→W0.2 window. W0.2 retires the interim and makes the brain the
//! first-host (it sets `status=online` on a live `listen`/`bind`); from then this
//! reconcile reacts within one tick, the same way the shell waker reacts to a
//! CLI-process status flip. The full bind→online→brain-restart→re-host arc is
//! proven through the real brain process by the W0.4 production int.
// [impl->REQ-DAEMON-1]

use std::collections::HashMap;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::Duration;

use spt_runtime::registry::AdapterRecord;
use spt_runtime::Manifest;
use spt_store::liveness::STATUS_ONLINE;
use spt_store::perch::{self, ParentHint};

use crate::brainproc::StartReason;
use crate::config::DaemonConfig;
use crate::lifecycle::BrainLifecycle;

/// The `InfoJson::state` tag of a live agent — only these get a daemon-hosted
/// Psyche; a `ready_agent` (messaging-only) never does.
const LIVE_AGENT_STATE: &str = "live_agent";

/// The reconcile cadence (ms): a lifecycle flip lands in a CLI process, so the
/// daemon loop is the invariant holder — a flip is at most one tick stale
/// (mirrors [`crate::shellwake::RECONCILE_INTERVAL_MS`]).
pub const LIVE_RECONCILE_INTERVAL_MS: u64 = 5_000;

/// One brain-hosted live lifecycle: its config-paced pulse-driver thread plus the
/// held stop flag (tripped to un-host on an offline-transition).
struct HostedLife {
    stop: Arc<AtomicBool>,
    thread: JoinHandle<()>,
}

/// The brain's hosted live-endpoint table, keyed by endpoint id. Shared between
/// the reconcile loop and anything that must stop a host. Mirror of
/// [`crate::shellwake::WakeSet`].
#[derive(Default)]
pub struct LiveSet {
    inner: Mutex<HashMap<String, HostedLife>>,
}

impl LiveSet {
    pub fn new() -> Self {
        Self::default()
    }

    /// How many lifecycles are hosted (finished driver threads pruned first).
    pub fn len(&self) -> usize {
        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
        map.retain(|_, h| !h.thread.is_finished());
        map.len()
    }

    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// Is `id` currently hosted (driver thread alive)? Prunes a finished entry so
    /// a crashed driver is re-hosted next sweep.
    fn contains(&self, id: &str) -> bool {
        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
        match map.get(id) {
            Some(h) if !h.thread.is_finished() => true,
            Some(_) => {
                map.remove(id);
                false
            }
            None => false,
        }
    }

    fn insert(&self, id: &str, handle: HostedLife) {
        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
        map.insert(id.to_string(), handle);
    }

    /// The currently-tabled ids (finished pruned) — the stop-side reconcile input.
    fn ids(&self) -> Vec<String> {
        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
        map.retain(|_, h| !h.thread.is_finished());
        map.keys().cloned().collect()
    }

    /// Un-host one lifecycle: trip its stop flag, join the driver, de-table. The
    /// pulse loop checks the flag each sleep-slice, so the join is prompt. We do
    /// NOT re-stamp `offline` — the perch already left the online set (graceful
    /// signoff cleared it, or the perch is gone); a brain *crash* deliberately
    /// leaves `status=online` so the next brain rehydrates it (CONTEXT:194).
    fn stop_host(&self, id: &str) {
        let handle = {
            let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
            map.remove(id)
        };
        if let Some(h) = handle {
            h.stop.store(true, Ordering::SeqCst);
            let _ = h.thread.join();
        }
    }
}

/// One reconcile sweep — the brain's live host-set invariant holder, run at boot
/// and every [`LIVE_RECONCILE_INTERVAL_MS`]. The complete set-diff against the
/// on-disk **online** live endpoints (mirror of [`crate::shellwake::reconcile_once`]):
///
/// - **un-host** (tabled ∧ ¬online) — stop the driver + de-table (exit guard: a
///   signed-off / removed endpoint must not keep pulsing — thread + dead-agent leak).
/// - **host** (online ∧ ¬tabled) — spawn the Psyche + start the driver.
/// - **leave-alone** (online ∧ tabled).
///
/// `status` is daemon-authoritative (CONTEXT:194); interim `listen` perches are
/// `status`-ABSENT so they are never hosted here (no double-host) until W0.2.
// [impl->REQ-DAEMON-1]
pub fn reconcile_once(
    owlery: &Path,
    registered: &[(AdapterRecord, Manifest)],
    adapters_dir: &Path,
    set: &Arc<LiveSet>,
    cfg: &DaemonConfig,
    reason: StartReason,
) {
    // Stop side first: hosted endpoints that left the online set (signed off /
    // perch gone) — trip the flag, join, de-table.
    for id in set.ids() {
        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);
        let still_online = spt_store::info::read_info(&perch)
            .map(|i| i.status.as_deref() == Some(STATUS_ONLINE))
            .unwrap_or(false);
        if !still_online {
            set.stop_host(&id);
        }
    }

    // Start side: every online live endpoint not yet hosted.
    for id in perch::list_self_perch_ids(owlery) {
        if set.contains(&id) {
            continue; // already hosted (leave-alone)
        }
        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);
        let Some(info) = spt_store::info::read_info(&perch) else {
            continue;
        };
        if info.state != LIVE_AGENT_STATE {
            continue; // not a live agent (ready_agent etc.)
        }
        if info.status.as_deref() != Some(STATUS_ONLINE) {
            continue; // interim (status-absent) or signed-off (offline)
        }
        let Some(adapter) = info.adapter.as_deref() else {
            continue; // adapterless endpoint — never live-capable
        };
        let Ok(manifest) = spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter)
        else {
            continue; // deregistered adapter: nothing to host
        };
        let Some(lifecycle) = BrainLifecycle::with_config(&manifest, &id, cfg.clone()) else {
            continue; // manifest declares no [session.psyche_init]
        };
        host_one(set, &id, lifecycle, info.session_id, reason);
    }
}

/// Host one live endpoint: sweep a stale signoff (3.2), spawn the Psyche (the
/// daemon online-stamp makes liveness daemon-authoritative), then start the
/// config-paced pulse driver on its own supervised thread with a held stop flag.
/// A Psyche-spawn failure tables nothing — the next sweep retries.
// [impl->REQ-DAEMON-1]
fn host_one(
    set: &Arc<LiveSet>,
    id: &str,
    lifecycle: BrainLifecycle,
    session_id: String,
    reason: StartReason,
) {
    lifecycle.sweep_on_start();
    match lifecycle.spawn_psyche(Some(&session_id), None) {
        Ok(h) => eprintln!("LIVEHOST_PSYCHE:{id} pid={}", h.pid),
        Err(e) => {
            eprintln!("LIVEHOST_PSYCHE_FAIL:{id}: {e}");
            return; // no Psyche → no driver; retried next reconcile
        }
    }
    let stop = Arc::new(AtomicBool::new(false));
    let handle = {
        let stop = Arc::clone(&stop);
        std::thread::spawn(move || {
            lifecycle.run_pulse_loop(Some(&session_id), &stop, reason, |_report| {});
        })
    };
    set.insert(id, HostedLife { stop, thread: handle });
}

/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot
/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +
/// config are re-read each sweep (adapter / pulse-period changes land between
/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held
/// for symmetry — the threads die with the brain process and the next brain
/// rehydrates from disk.
// [impl->REQ-DAEMON-1]
pub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {
    std::thread::spawn(move || {
        let set = Arc::new(LiveSet::new());
        while !stop.load(Ordering::SeqCst) {
            let owlery = perch::owlery_dir();
            let adapters_dir = perch::adapters_dir();
            let registered = spt_runtime::registry::registered(&adapters_dir);
            let cfg = DaemonConfig::load();
            reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);
            // Sleep in slices so a stop lands promptly.
            let mut left = LIVE_RECONCILE_INTERVAL_MS;
            while left > 0 && !stop.load(Ordering::SeqCst) {
                let step = left.min(100);
                std::thread::sleep(Duration::from_millis(step));
                left -= step;
            }
        }
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_home::with_home;
    use std::time::{Duration, Instant};

    // A no-op Psyche summarizer command: spawns + exits 0 (we never depend on its
    // output — the daemon online-stamp is what hosts it, not the pid).
    #[cfg(windows)]
    const NOOP_CMD: &str = "cmd /C rem";
    #[cfg(unix)]
    const NOOP_CMD: &str = "true";

    /// Register a live-capable adapter `name` (declares `[session.psyche_init]` +
    /// a watched commune/signoff dir) so `resolve_option_in` finds it from disk.
    fn register_live_adapter(name: &str, drops: &Path) {
        let dir = drops.to_string_lossy().replace('\\', "/");
        let toml = format!(
            "[adapter]\nname=\"{name}\"\nkind=\"harness\"\nversion=\"1\"\n\
             min_spt_core_version=\"0\"\n\n\
             [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
        );
        let src = perch::spt_home().join("srcs").join(name);
        std::fs::create_dir_all(&src).unwrap();
        std::fs::write(src.join("manifest.toml"), toml).unwrap();
        spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
    }

    /// Seed a Self **live** perch on disk: `state=live_agent`, the given status,
    /// and the adapter option that resolves the manifest.
    fn seed_live_perch(id: &str, adapter: &str, status: &str) {
        let perch = perch::resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&perch).unwrap();
        let mut rec = spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid-1", "live_agent");
        rec.adapter = Some(adapter.to_string());
        spt_store::info::write_info(&perch, &rec).unwrap();
        spt_store::info::set_status(&perch, status).unwrap();
    }

    /// A fast-pulse config so the driver loop ticks quickly in tests.
    fn fast_cfg() -> DaemonConfig {
        DaemonConfig {
            pulse_period: Duration::from_millis(15),
            ..DaemonConfig::default()
        }
    }

    fn registered_now() -> Vec<(AdapterRecord, Manifest)> {
        spt_runtime::registry::registered(&perch::adapters_dir())
    }

    /// Poll a predicate up to `bound`, sleeping 20ms between checks.
    fn wait_until(bound: Duration, mut pred: impl FnMut() -> bool) -> bool {
        let deadline = Instant::now() + bound;
        while Instant::now() < deadline {
            if pred() {
                return true;
            }
            std::thread::sleep(Duration::from_millis(20));
        }
        pred()
    }

    // [unit->REQ-DAEMON-1] (a) FIRST-HOST scan: an online live endpoint on disk is
    // hosted by the reconcile — the Psyche is spawned AND the config-paced pulse
    // driver actually runs (it stamps last_active_ms on the Self perch each tick).
    #[test]
    fn reconcile_hosts_an_online_live_endpoint_and_drives_its_pulse() {
        with_home(|_| {
            let drops = perch::spt_home().join("drops");
            std::fs::create_dir_all(&drops).unwrap();
            register_live_adapter("mock", &drops);
            seed_live_perch("doyle", "mock", STATUS_ONLINE);

            let set = Arc::new(LiveSet::new());
            let cfg = fast_cfg();
            reconcile_once(
                &perch::owlery_dir(),
                &registered_now(),
                &perch::adapters_dir(),
                &set,
                &cfg,
                StartReason::Cold,
            );
            assert_eq!(set.len(), 1, "the online live endpoint is hosted");

            // The driver pulse runs: an Active perch stamps last_active_ms each tick.
            let self_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
            let ran = wait_until(Duration::from_secs(3), || {
                spt_store::info::read_info(&self_perch)
                    .and_then(|i| i.last_active_ms)
                    .is_some()
            });
            assert!(ran, "the pulse driver loop ticks (stamps last_active_ms)");

            // Teardown so the test process doesn't leak the driver thread.
            set.stop_host("doyle");
        });
    }

    // [unit->REQ-DAEMON-1] (b) ENTRY GUARD: a never-hosted OFFLINE live endpoint is
    // skipped — online-only. (Also covers the interim status-absent case shape.)
    #[test]
    fn reconcile_skips_an_offline_live_endpoint() {
        with_home(|_| {
            let drops = perch::spt_home().join("drops");
            std::fs::create_dir_all(&drops).unwrap();
            register_live_adapter("mock", &drops);
            seed_live_perch("doyle", "mock", spt_store::liveness::STATUS_OFFLINE);

            let set = Arc::new(LiveSet::new());
            reconcile_once(
                &perch::owlery_dir(),
                &registered_now(),
                &perch::adapters_dir(),
                &set,
                &fast_cfg(),
                StartReason::Cold,
            );
            assert!(set.is_empty(), "an offline live endpoint is never hosted");
        });
    }

    // [unit->REQ-DAEMON-1] (c) NO DOUBLE-HOST: a second reconcile over an
    // already-tabled endpoint leaves exactly one host (the contains-guard).
    #[test]
    fn reconcile_does_not_double_host() {
        with_home(|_| {
            let drops = perch::spt_home().join("drops");
            std::fs::create_dir_all(&drops).unwrap();
            register_live_adapter("mock", &drops);
            seed_live_perch("doyle", "mock", STATUS_ONLINE);

            let set = Arc::new(LiveSet::new());
            let cfg = fast_cfg();
            let run = || {
                reconcile_once(
                    &perch::owlery_dir(),
                    &registered_now(),
                    &perch::adapters_dir(),
                    &set,
                    &cfg,
                    StartReason::Cold,
                )
            };
            run();
            run();
            assert_eq!(set.len(), 1, "the same endpoint is hosted exactly once");
            set.stop_host("doyle");
        });
    }

    // [unit->REQ-DAEMON-1] (d) REHYDRATE: a FRESH LiveSet (a restarted brain) over a
    // perch left `status=online` by a prior brain re-hosts it — distinct from (a)
    // in that the table starts empty against an already-online perch (the
    // brain-restart path), not a freshly-flipped one.
    #[test]
    fn fresh_brain_rehydrates_an_online_live_endpoint() {
        with_home(|_| {
            let drops = perch::spt_home().join("drops");
            std::fs::create_dir_all(&drops).unwrap();
            register_live_adapter("mock", &drops);
            seed_live_perch("doyle", "mock", STATUS_ONLINE);

            // First brain hosts it.
            let first = Arc::new(LiveSet::new());
            let cfg = fast_cfg();
            reconcile_once(
                &perch::owlery_dir(),
                &registered_now(),
                &perch::adapters_dir(),
                &first,
                &cfg,
                StartReason::Cold,
            );
            assert_eq!(first.len(), 1);
            // Brain "restart": drop the table (threads die with the process), the
            // perch stays online on disk.
            first.stop_host("doyle");
            drop(first);

            // Fresh brain rehydrates from the still-online perch.
            let second = Arc::new(LiveSet::new());
            reconcile_once(
                &perch::owlery_dir(),
                &registered_now(),
                &perch::adapters_dir(),
                &second,
                &cfg,
                StartReason::Crash,
            );
            assert_eq!(second.len(), 1, "a fresh brain re-hosts the online endpoint");
            set_then_teardown(&second);
        });
    }

    fn set_then_teardown(set: &Arc<LiveSet>) {
        set.stop_host("doyle");
    }

    // [unit->REQ-DAEMON-1] (e) EXIT GUARD / UN-HOST: a hosted endpoint that flips to
    // status=offline (graceful signoff) is un-hosted by the next reconcile — the
    // driver thread is stopped + de-tabled (no forever-pulsing dead agent).
    #[test]
    fn reconcile_unhosts_on_offline_transition() {
        with_home(|_| {
            let drops = perch::spt_home().join("drops");
            std::fs::create_dir_all(&drops).unwrap();
            register_live_adapter("mock", &drops);
            seed_live_perch("doyle", "mock", STATUS_ONLINE);

            let set = Arc::new(LiveSet::new());
            let cfg = fast_cfg();
            let run = |reason| {
                reconcile_once(
                    &perch::owlery_dir(),
                    &registered_now(),
                    &perch::adapters_dir(),
                    &set,
                    &cfg,
                    reason,
                )
            };
            run(StartReason::Cold);
            assert_eq!(set.len(), 1, "hosted while online");

            // Graceful signoff flips the Self perch offline.
            let self_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
            spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_OFFLINE).unwrap();

            run(StartReason::Cold);
            assert!(
                set.is_empty(),
                "an offline-transitioned endpoint is un-hosted (driver stopped + de-tabled)"
            );
        });
    }
}


File created successfully at: crates\spt-daemon\src\livehost.rs (file state is current in your context — no need to Read it back)

//! The brain's live-agent Psyche host (M11-W0.1, REQ-DAEMON-1) — re-host the
//! daemon-hosted LiveAgent lifecycle from disk and reconcile the host-set.
//!
//! This is the production caller `run_pulse_loop` lacked: the brain instantiates
//! a [`BrainLifecycle`] per hosted **live** endpoint and drives its config-paced
//! pulse loop on a supervised thread. It is the **harness-endpoint mirror** of
//! [`crate::shellwake::reconcile_once`] (which re-hosts offline shells from
//! disk): same shape — enumerate Self perches, resolve each manifest from its
//! `info.json` `adapter` option, hold a stop flag per supervised thread, and run
//! one bounded reconcile sweep at boot and every tick. Where the shell waker
//! hosts **offline** shells, this hosts **online** live endpoints — `status` is
//! daemon-authoritative liveness (CONTEXT:194 / KH 2.5), so re-hosting the
//! online ones re-hosts exactly what was live before a brain (re)start, and
//! never resurrects a gracefully-departed (offline) agent.
//!
//! **Online-only is staged behind the interim.** The interim `spt api listen`
//! process still first-hosts its live agents today; those perches are
//! `status`-ABSENT (not "online"), so this scan ignores them — no double-host in
//! the W0.1→W0.2 window. W0.2 retires the interim and makes the brain the
//! first-host (it sets `status=online` on a live `listen`/`bind`); from then this
//! reconcile reacts within one tick, the same way the shell waker reacts to a
//! CLI-process status flip. The full bind→online→brain-restart→re-host arc is
//! proven through the real brain process by the W0.4 production int.
// [impl->REQ-DAEMON-1]

use std::collections::HashMap;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::Duration;

use spt_runtime::registry::AdapterRecord;
use spt_runtime::Manifest;
use spt_store::liveness::STATUS_ONLINE;
use spt_store::perch::{self, ParentHint};

use crate::brainproc::StartReason;
use crate::config::DaemonConfig;
use crate::lifecycle::BrainLifecycle;

/// The `InfoJson::state` tag of a live agent — only these get a daemon-hosted
/// Psyche; a `ready_agent` (messaging-only) never does.
const LIVE_AGENT_STATE: &str = "live_agent";

/// The reconcile cadence (ms): a lifecycle flip lands in a CLI process, so the
/// daemon loop is the invariant holder — a flip is at most one tick stale
/// (mirrors [`crate::shellwake::RECONCILE_INTERVAL_MS`]).
pub const LIVE_RECONCILE_INTERVAL_MS: u64 = 5_000;

/// One brain-hosted live lifecycle: its config-paced pulse-driver thread plus the
/// held stop flag (tripped to un-host on an offline-transition).
struct HostedLife {
    stop: Arc<AtomicBool>,
    thread: JoinHandle<()>,
}

/// The brain's hosted live-endpoint table, keyed by endpoint id. Shared between
/// the reconcile loop and anything that must stop a host. Mirror of
/// [`crate::shellwake::WakeSet`].
#[derive(Default)]
pub struct LiveSet {
    inner: Mutex<HashMap<String, HostedLife>>,
}

impl LiveSet {
    pub fn new() -> Self {
        Self::default()
    }

    /// How many lifecycles are hosted (finished driver threads pruned first).
    pub fn len(&self) -> usize {
        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
        map.retain(|_, h| !h.thread.is_finished());
        map.len()
    }

    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// Is `id` currently hosted (driver thread alive)? Prunes a finished entry so
    /// a crashed driver is re-hosted next sweep.
    fn contains(&self, id: &str) -> bool {
        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
        match map.get(id) {
            Some(h) if !h.thread.is_finished() => true,
            Some(_) => {
                map.remove(id);
                false
            }
            None => false,
        }
    }

    fn insert(&self, id: &str, handle: HostedLife) {
        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
        map.insert(id.to_string(), handle);
    }

    /// The currently-tabled ids (finished pruned) — the stop-side reconcile input.
    fn ids(&self) -> Vec<String> {
        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
        map.retain(|_, h| !h.thread.is_finished());
        map.keys().cloned().collect()
    }

    /// Un-host one lifecycle: trip its stop flag, join the driver, de-table. The
    /// pulse loop checks the flag each sleep-slice, so the join is prompt. We do
    /// NOT re-stamp `offline` — the perch already left the online set (graceful
    /// signoff cleared it, or the perch is gone); a brain *crash* deliberately
    /// leaves `status=online` so the next brain rehydrates it (CONTEXT:194).
    fn stop_host(&self, id: &str) {
        let handle = {
            let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
            map.remove(id)
        };
        if let Some(h) = handle {
            h.stop.store(true, Ordering::SeqCst);
            let _ = h.thread.join();
        }
    }
}

/// One reconcile sweep — the brain's live host-set invariant holder, run at boot
/// and every [`LIVE_RECONCILE_INTERVAL_MS`]. The complete set-diff against the
/// on-disk **online** live endpoints (mirror of [`crate::shellwake::reconcile_once`]):
///
/// - **un-host** (tabled ∧ ¬online) — stop the driver + de-table (exit guard: a
///   signed-off / removed endpoint must not keep pulsing — thread + dead-agent leak).
/// - **host** (online ∧ ¬tabled) — spawn the Psyche + start the driver.
/// - **leave-alone** (online ∧ tabled).
///
/// `status` is daemon-authoritative (CONTEXT:194); interim `listen` perches are
/// `status`-ABSENT so they are never hosted here (no double-host) until W0.2.
// [impl->REQ-DAEMON-1]
pub fn reconcile_once(
    owlery: &Path,
    registered: &[(AdapterRecord, Manifest)],
    adapters_dir: &Path,
    set: &Arc<LiveSet>,
    cfg: &DaemonConfig,
    reason: StartReason,
) {
    // Stop side first: hosted endpoints that left the online set (signed off /
    // perch gone) — trip the flag, join, de-table.
    for id in set.ids() {
        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);
        let still_online = spt_store::info::read_info(&perch)
            .map(|i| i.status.as_deref() == Some(STATUS_ONLINE))
            .unwrap_or(false);
        if !still_online {
            set.stop_host(&id);
        }
    }

    // Start side: every online live endpoint not yet hosted.
    for id in perch::list_self_perch_ids(owlery) {
        if set.contains(&id) {
            continue; // already hosted (leave-alone)
        }
        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);
        let Some(info) = spt_store::info::read_info(&perch) else {
            continue;
        };
        if info.state != LIVE_AGENT_STATE {
            continue; // not a live agent (ready_agent etc.)
        }
        if info.status.as_deref() != Some(STATUS_ONLINE) {
            continue; // interim (status-absent) or signed-off (offline)
        }
        let Some(adapter) = info.adapter.as_deref() else {
            continue; // adapterless endpoint — never live-capable
        };
        let Ok(manifest) = spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter)
        else {
            continue; // deregistered adapter: nothing to host
        };
        let Some(lifecycle) = BrainLifecycle::with_config(&manifest, &id, cfg.clone()) else {
            continue; // manifest declares no [session.psyche_init]
        };
        host_one(set, &id, lifecycle, info.session_id, reason);
    }
}

/// Host one live endpoint: sweep a stale signoff (3.2), spawn the Psyche (the
/// daemon online-stamp makes liveness daemon-authoritative), then start the
/// config-paced pulse driver on its own supervised thread with a held stop flag.
/// A Psyche-spawn failure tables nothing — the next sweep retries.
// [impl->REQ-DAEMON-1]
fn host_one(
    set: &Arc<LiveSet>,
    id: &str,
    lifecycle: BrainLifecycle,
    session_id: String,
    reason: StartReason,
) {
    lifecycle.sweep_on_start();
    match lifecycle.spawn_psyche(Some(&session_id), None) {
        Ok(h) => eprintln!("LIVEHOST_PSYCHE:{id} pid={}", h.pid),
        Err(e) => {
            eprintln!("LIVEHOST_PSYCHE_FAIL:{id}: {e}");
            return; // no Psyche → no driver; retried next reconcile
        }
    }
    let stop = Arc::new(AtomicBool::new(false));
    let handle = {
        let stop = Arc::clone(&stop);
        std::thread::spawn(move || {
            lifecycle.run_pulse_loop(Some(&session_id), &stop, reason, |_report| {});
        })
    };
    set.insert(id, HostedLife { stop, thread: handle });
}

/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot
/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +
/// config are re-read each sweep (adapter / pulse-period changes land between
/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held
/// for symmetry — the threads die with the brain process and the next brain
/// rehydrates from disk.
// [impl->REQ-DAEMON-1]
pub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {
    std::thread::spawn(move || {
        let set = Arc::new(LiveSet::new());
        while !stop.load(Ordering::SeqCst) {
            let owlery = perch::owlery_dir();
            let adapters_dir = perch::adapters_dir();
            let registered = spt_runtime::registry::registered(&adapters_dir);
            let cfg = DaemonConfig::load();
            reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);
            // Sleep in slices so a stop lands promptly.
            let mut left = LIVE_RECONCILE_INTERVAL_MS;
            while left > 0 && !stop.load(Ordering::SeqCst) {
                let step = left.min(100);
                std::thread::sleep(Duration::from_millis(step));
                left -= step;
            }
        }
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_home::with_home;
    use std::time::{Duration, Instant};

    // A no-op Psyche summarizer command: spawns + exits 0 (we never depend on its
    // output — the daemon online-stamp is what hosts it, not the pid).
    #[cfg(windows)]
    const NOOP_CMD: &str = "cmd /C rem";
    #[cfg(unix)]
    const NOOP_CMD: &str = "true";

    /// Register a live-capable adapter `name` (declares `[session.psyche_init]` +
    /// a watched commune/signoff dir) so `resolve_option_in` finds it from disk.
    fn register_live_adapter(name: &str, drops: &Path) {
        let dir = drops.to_string_lossy().replace('\\', "/");
        let toml = format!(
            "[adapter]\nname=\"{name}\"\nkind=\"harness\"\nversion=\"1\"\n\
             min_spt_core_version=\"0\"\n\n\
             [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
        );
        let src = perch::spt_home().join("srcs").join(name);
        std::fs::create_dir_all(&src).unwrap();
        std::fs::write(src.join("manifest.toml"), toml).unwrap();
        spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
    }

    /// Seed a Self **live** perch on disk: `state=live_agent`, the given status,
    /// and the adapter option that resolves the manifest.
    fn seed_live_perch(id: &str, adapter: &str, status: &str) {
        let perch = perch::resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&perch).unwrap();
        let mut rec = spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid-1", "live_agent");
        rec.adapter = Some(adapter.to_string());
        spt_store::info::write_info(&perch, &rec).unwrap();
        spt_store::info::set_status(&perch, status).unwrap();
    }

    /// A fast-pulse config so the driver loop ticks quickly in tests.
    fn fast_cfg() -> DaemonConfig {
        DaemonConfig {
            pulse_period: Duration::from_millis(15),
            ..DaemonConfig::default()
        }
    }

    fn registered_now() -> Vec<(AdapterRecord, Manifest)> {
        spt_runtime::registry::registered(&perch::adapters_dir())
    }

    /// Poll a predicate up to `bound`, sleeping 20ms between checks.
    fn wait_until(bound: Duration, mut pred: impl FnMut() -> bool) -> bool {
        let deadline = Instant::now() + bound;
        while Instant::now() < deadline {
            if pred() {
                return true;
            }
            std::thread::sleep(Duration::from_millis(20));
        }
        pred()
    }

    // [unit->REQ-DAEMON-1] (a) FIRST-HOST scan: an online live endpoint on disk is
    // hosted by the reconcile — the Psyche is spawned AND the config-paced pulse
    // driver actually runs (it stamps last_active_ms on the Self perch each tick).
    #[test]
    fn reconcile_hosts_an_online_live_endpoint_and_drives_its_pulse() {
        with_home(|_| {
            let drops = perch::spt_home().join("drops");
            std::fs::create_dir_all(&drops).unwrap();
            register_live_adapter("mock", &drops);
            seed_live_perch("doyle", "mock", STATUS_ONLINE);

            let set = Arc::new(LiveSet::new());
            let cfg = fast_cfg();
            reconcile_once(
                &perch::owlery_dir(),
                &registered_now(),
                &perch::adapters_dir(),
                &set,
                &cfg,
                StartReason::Cold,
            );
            assert_eq!(set.len(), 1, "the online live endpoint is hosted");

            // The driver pulse runs: an Active perch stamps last_active_ms each tick.
            let self_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
            let ran = wait_until(Duration::from_secs(3), || {
                spt_store::info::read_info(&self_perch)
                    .and_then(|i| i.last_active_ms)
                    .is_some()
            });
            assert!(ran, "the pulse driver loop ticks (stamps last_active_ms)");

            // Teardown so the test process doesn't leak the driver thread.
            set.stop_host("doyle");
        });
    }

    // [unit->REQ-DAEMON-1] (b) ENTRY GUARD: a never-hosted OFFLINE live endpoint is
    // skipped — online-only. (Also covers the interim status-absent case shape.)
    #[test]
    fn reconcile_skips_an_offline_live_endpoint() {
        with_home(|_| {
            let drops = perch::spt_home().join("drops");
            std::fs::create_dir_all(&drops).unwrap();
            register_live_adapter("mock", &drops);
            seed_live_perch("doyle", "mock", spt_store::liveness::STATUS_OFFLINE);

            let set = Arc::new(LiveSet::new());
            reconcile_once(
                &perch::owlery_dir(),
                &registered_now(),
                &perch::adapters_dir(),
                &set,
                &fast_cfg(),
                StartReason::Cold,
            );
            assert!(set.is_empty(), "an offline live endpoint is never hosted");
        });
    }

    // [unit->REQ-DAEMON-1] (c) NO DOUBLE-HOST: a second reconcile over an
    // already-tabled endpoint leaves exactly one host (the contains-guard).
    #[test]
    fn reconcile_does_not_double_host() {
        with_home(|_| {
            let drops = perch::spt_home().join("drops");
            std::fs::create_dir_all(&drops).unwrap();
            register_live_adapter("mock", &drops);
            seed_live_perch("doyle", "mock", STATUS_ONLINE);

            let set = Arc::new(LiveSet::new());
            let cfg = fast_cfg();
            let run = || {
                reconcile_once(
                    &perch::owlery_dir(),
                    &registered_now(),
                    &perch::adapters_dir(),
                    &set,
                    &cfg,
                    StartReason::Cold,
                )
            };
            run();
            run();
            assert_eq!(set.len(), 1, "the same endpoint is hosted exactly once");
            set.stop_host("doyle");
        });
    }

    // [unit->REQ-DAEMON-1] (d) REHYDRATE: a FRESH LiveSet (a restarted brain) over a
    // perch left `status=online` by a prior brain re-hosts it — distinct from (a)
    // in that the table starts empty against an already-online perch (the
    // brain-restart path), not a freshly-flipped one.
    #[test]
    fn fresh_brain_rehydrates_an_online_live_endpoint() {
        with_home(|_| {
            let drops = perch::spt_home().join("drops");
            std::fs::create_dir_all(&drops).unwrap();
            register_live_adapter("mock", &drops);
            seed_live_perch("doyle", "mock", STATUS_ONLINE);

            // First brain hosts it.
            let first = Arc::new(LiveSet::new());
            let cfg = fast_cfg();
            reconcile_once(
                &perch::owlery_dir(),
                &registered_now(),
                &perch::adapters_dir(),
                &first,
                &cfg,
                StartReason::Cold,
            );
            assert_eq!(first.len(), 1);
            // Brain "restart": drop the table (threads die with the process), the
            // perch stays online on disk.
            first.stop_host("doyle");
            drop(first);

            // Fresh brain rehydrates from the still-online perch.
            let second = Arc::new(LiveSet::new());
            reconcile_once(
                &perch::owlery_dir(),
                &registered_now(),
                &perch::adapters_dir(),
                &second,
                &cfg,
                StartReason::Crash,
            );
            assert_eq!(second.len(), 1, "a fresh brain re-hosts the online endpoint");
            set_then_teardown(&second);
        });
    }

    fn set_then_teardown(set: &Arc<LiveSet>) {
        set.stop_host("doyle");
    }

    // [unit->REQ-DAEMON-1] (e) EXIT GUARD / UN-HOST: a hosted endpoint that flips to
    // status=offline (graceful signoff) is un-hosted by the next reconcile — the
    // driver thread is stopped + de-tabled (no forever-pulsing dead agent).
    #[test]
    fn reconcile_unhosts_on_offline_transition() {
        with_home(|_| {
            let drops = perch::spt_home().join("drops");
            std::fs::create_dir_all(&drops).unwrap();
            register_live_adapter("mock", &drops);
            seed_live_perch("doyle", "mock", STATUS_ONLINE);

            let set = Arc::new(LiveSet::new());
            let cfg = fast_cfg();
            let run = |reason| {
                reconcile_once(
                    &perch::owlery_dir(),
                    &registered_now(),
                    &perch::adapters_dir(),
                    &set,
                    &cfg,
                    reason,
                )
            };
            run(StartReason::Cold);
            assert_eq!(set.len(), 1, "hosted while online");

            // Graceful signoff flips the Self perch offline.
            let self_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
            spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_OFFLINE).unwrap();

            run(StartReason::Cold);
            assert!(
                set.is_empty(),
                "an offline-transitioned endpoint is un-hosted (driver stopped + de-tabled)"
            );
        });
    }
}


Wire it: add `pub mod livehost;` to lib.rs + spawn it in `run_brain` (beside shellwake) + update the no-op comment.


Edit

pub mod linkhost;
pub mod livehost;
pub mod machineid;


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));

    let mut consumers_started = false;

    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));

    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller
    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at
    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting
    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the
    // shell waker). Online-only is staged behind the interim `api listen`
    // (status-absent perches are skipped — no double-host) until W0.2 makes the
    // brain the first-host. The stop flag is held for symmetry; the threads die
    // with the brain and the next brain rehydrates from disk (CONTEXT:194).
    // [impl->REQ-DAEMON-1]
    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);

    let mut consumers_started = false;


The file crates\spt-daemon\src\brainproc.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));

    let mut consumers_started = false;

    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));

    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller
    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at
    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting
    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the
    // shell waker). Online-only is staged behind the interim `api listen`
    // (status-absent perches are skipped — no double-host) until W0.2 makes the
    // brain the first-host. The stop flag is held for symmetry; the threads die
    // with the brain and the next brain rehydrates from disk (CONTEXT:194).
    // [impl->REQ-DAEMON-1]
    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);

    let mut consumers_started = false;

+    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller

+    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at

+    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting

+    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the

+    // shell waker). Online-only is staged behind the interim `api listen`

+    // (status-absent perches are skipped — no double-host) until W0.2 makes the

+    // brain the first-host. The stop flag is held for symmetry; the threads die

+    // with the brain and the next brain rehydrates from disk (CONTEXT:194).

+    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);


Read lib.rs slice to edit the module decl + update the no-op comment.


128	pub mod linkhost;
129	pub mod machineid;

pub mod linkhost;
pub mod machineid;


175	    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild
176	    // continuity from the broker — re-attach EVERY hosted session in resume mode
177	    // from its delivered cursor, the production replacement for the retired
178	    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no
179	    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),
180	    // so this is a no-op now and forward-correct when daemon-hosted sessions land
181	    // (the live-agent adapter). A failure here is non-fatal — the brain still
182	    // serves; the next heartbeat probes broker liveness.
183	    match brain.resume_sessions() {
184	        Ok(ids) if !ids.is_empty() => {

    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild
    // continuity from the broker — re-attach EVERY hosted session in resume mode
    // from its delivered cursor, the production replacement for the retired
    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no
    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),
    // so this is a no-op now and forward-correct when daemon-hosted sessions land
    // (the live-agent adapter). A failure here is non-fatal — the brain still
    // serves; the next heartbeat probes broker liveness.
    match brain.resume_sessions() {
        Ok(ids) if !ids.is_empty() => {


pub mod linkhost;
pub mod livehost;
pub mod machineid;


The file crates\spt-daemon\src\lib.rs has been updated successfully. (file state is current in your context — no need to Read it back)

pub mod linkhost;
pub mod livehost;
pub mod machineid;

pub mod harnesshost;
pub mod lifecycle;
pub mod linkhost;
+pub mod livehost;
pub mod machineid;
pub mod msg;
pub mod nethost;


    // so this is a no-op now and forward-correct when daemon-hosted sessions land
    // (the live-agent adapter). A failure here is non-fatal — the brain still
    // serves; the next heartbeat probes broker liveness.

    // so this is a no-op now and forward-correct when daemon-hosted sessions land
    // (the live-agent adapter). A failure here is non-fatal — the brain still
    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY
    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the
    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`
    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).


    // so this is a no-op now and forward-correct when daemon-hosted sessions land
    // (the live-agent adapter). A failure here is non-fatal — the brain still
    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY
    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the
    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`
    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).

// PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),

-    // serves; the next heartbeat probes broker liveness.

+    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY

+    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the

+    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`

+    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).


Build + run the livehost units.


Bash

cargo test -p spt-daemon --lib livehost 2>&1 | tail -30

Build + run livehost unit tests


   Compiling spt-daemon v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 27.65s
     Running unittests src\lib.rs (target\debug\deps\spt_daemon-1ca149622a70a887.exe)

running 5 tests
test livehost::tests::fresh_brain_rehydrates_an_online_live_endpoint ... ok
test livehost::tests::reconcile_hosts_an_online_live_endpoint_and_drives_its_pulse ... ok
test livehost::tests::reconcile_does_not_double_host ... ok
test livehost::tests::reconcile_skips_an_offline_live_endpoint ... ok
test livehost::tests::reconcile_unhosts_on_offline_transition ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 288 filtered out; finished in 0.40s


All 5 livehost units green (a–e). Pre-flight clippy `--workspace` (the lesson) + traceable, before committing W0.1.


cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -6

Workspace clippy pre-flight


traceable-reqs check 2>&1 | grep -iE "REQ-DAEMON-1\b|error|FAIL|missing" | head; echo "EXIT=${PIPESTATUS[0]}"

traceable check REQ-DAEMON-1


    Checking spt-daemon v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Checking spt v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Checking xtask v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\xtask)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 9.07s


       Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)
  [OK] REQ-DAEMON-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Net-bind boot-race resilience: a daemon that comes up net-less (NetHost::start failed — e.g. the systemd unit autostarted before the network/DNS stack was ready, `Failed to create an address lookup service`) must SELF-HEAL — retry the net bring-up in the background with capped backoff and, on success, attach net to the broker + spawn the dispatcher/peer-pump (which today are gated on `net_up` at boot and so never start, leaving the node silently unreachable until a manual restart — kitsubito 2026-06-08). Status surfaces the net-less state honestly (a net-less broker renders as 'no connection', not only a pump-STALLED line with a bogus pre-boot heartbeat age). The installer's autostart unit waits for the network (`Wants=/After=network-online.target`) as belt-and-suspenders.
       A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)
       Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)
       Stale registry entries degrade to fallback, never hard-fail (4.3)
       File-backed adapter [strings] (M12-W3-T3.1): a [strings] dot-path value MAY be an inline-table FILE POINTER `key = { file = "rel/path" }` resolved to the file's contents at get-string time, keeping large bodies (skill-instructions, hint text) out of the manifest. A value-position table with a `file` key IS the pointer form (reserved — cannot double as data). Per-adapter aux storage `adapters/<adapter>/strings/`; pointers resolve relative to it with CONTAINMENT (reject `..`/absolute escaping the dir). UPDATE-SAFETY: a LOCAL profile's file-pointers resolve relative to the user-owned local-profile dir (NOT adapter-shipped strings/, which adapter updates overwrite), or the local profile inlines. Validate-at-register (fail-fast on a bad/escaping/missing pointer) + LAZY read at get-string (live file edits reflect, no re-register) + skip-diagnostics on missing-at-read (no hard-crash, mirrors [digest]). Rides the same leaf-replace profile overlay as the rest of [strings].
       cross-node Gateway user-msg honored via advertised endpoint_type: a user-msg from a Gateway-typed origin survives the receive_wan funnel as user-msg (vs the fail-closed re-stamp), keyed on the QUIC-handshake-proven origin node (never wire `from`). Trust boundary = subnet membership (operator-ratified 2026-06-13); no defense against an in-subnet member forging the type. Instance.endpoint_type is an additive serde-default field extending REQ-INST-7's data model. Absent/unknown type → re-stamp (N-1 rollout grace)
       NTP TOTP offset: the pairing ceremony queries NTP at ceremony time (both sides) and applies the derived offset to the TOTP calculation in-process only; system-clock fallback when NTP is unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop (M8 decision 18; field trigger: enlyzeam clock >1 min off exceeds the ±1 window)
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
EXIT=0


traceable EXIT 0, clippy clean. livehost adds `[impl/unit->REQ-DAEMON-1]` (int re-point stays W0.4). Run the full spt-daemon lib suite + daemon_e2e to confirm the brainproc wiring didn't regress.


cargo test -p spt-daemon --lib 2>&1 | tail -6

Full spt-daemon lib unit suite


cargo test -p spt-daemon --test daemon_e2e 2>&1 | tail -8

daemon_e2e integration (brain lifecycle)


test sync::tests::reconcile_driver_structurally_excludes_live_role ... ok
test shellwake::tests::watcher_crash_exits_respawn_then_give_up ... ok
test sync::tests::select_refs_scopes_two_tiers ... ok

test result: ok. 293 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.76s


    Finished `test` profile [unoptimized + debuginfo] target(s) in 11.67s
     Running tests\daemon_e2e.rs (target\debug\deps\daemon_e2e-eae42c2cc7b68b6d.exe)

running 1 test
test daemon_hosts_lifecycle_and_survives_brain_restart ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.45s


All green: 293 lib units (incl 5 livehost) + daemon_e2e, no regression. Commit W0.1.


git branch --show-current && git add -A && git status --short

Branch check + stage W0.1


m11-shell-substrate
warning: in the working copy of 'crates/spt-daemon/src/livehost.rs', LF will be replaced by CRLF the next time Git touches it
M  crates/spt-daemon/src/brainproc.rs
M  crates/spt-daemon/src/lib.rs
A  crates/spt-daemon/src/livehost.rs


git commit -q -F - <<'EOF'
M11-W0.1: brain hosts the live-agent Psyche lifecycle (REQ-DAEMON-1)

The production caller run_pulse_loop lacked. New crate::livehost re-hosts the
daemon-hosted LiveAgent lifecycle from disk — the harness-endpoint mirror of
shellwake::reconcile_once (which re-hosts offline shells). The brain instantiates
a BrainLifecycle per hosted LIVE endpoint and drives its config-paced pulse loop
on a supervised thread, replacing the brainproc no-op.

Design (doyle-gated, grounded vs CONTEXT:30/34/168/177/194):
- Manifest-from-disk: the Self perch's info.json `adapter` option resolves the
  manifest via registry::resolve_option_in (the linkhost/shellwake precedent) —
  no harness dependency; [session.psyche_init] decides live-capable.
- Two orthogonal axes: resume_sessions = broker PTY message DELIVERY (untouched);
  the Psyche endpoint-table = live Self perches enumerated via list_self_perch_ids
  (CONTEXT:168/177 decouple psyche-hosting from delivery).
- ONLINE-ONLY full set-diff reconcile (boot + tick): host (online ∧ ¬tabled),
  un-host (tabled ∧ ¬online — stop driver + de-table, no forever-pulsing dead
  agent), leave-alone (online ∧ tabled). status is daemon-authoritative
  (CONTEXT:194); re-host what was live, never resurrect a signed-off agent.
- Staged behind the interim: interim `api listen` perches are status-ABSENT so
  the scan ignores them (no double-host) until W0.2 makes the brain the
  first-host. W0.1's scan is dormant in production this wave.

Units a–e ([unit->REQ-DAEMON-1]): (a) first-host an online live endpoint + prove
the pulse driver actually ticks (stamps last_active_ms); (b) entry-guard skip an
offline endpoint; (c) no double-host; (d) fresh-brain rehydrate of an
already-online perch; (e) exit-guard un-host on offline-transition (driver
stopped + de-tabled). 293 spt-daemon lib units + daemon_e2e green; clippy
--workspace clean; traceable EXIT 0.

REQ-DAEMON-1 int re-point to a production-path E2E is W0.4 (the load-bearing
rung — real brain process, not BrainLifecycle-in-a-test); first-host
establish-marks-online is W0.2/W0.3.

Co-authored by: todlando
EOF
git log --oneline -1

Commit W0.1


1448a0a M11-W0.1: brain hosts the live-agent Psyche lifecycle (REQ-DAEMON-1)


W0.1 committed @1448a0a. Push (updates PR #16 → CI) + submit to doyle for the impl gate.


Push W0.1 to PR branch


To https://github.com/SaberMage/spt-core.git
   06cf6f1..1448a0a  m11-shell-substrate -> m11-shell-substrate


$OWL send doyle <<'EOF'
[todlando → doyle] W0.1 EVIDENCE-LAND — committed @1448a0a, pushed to PR #16 (CI running). Built to the gated design exactly.

NEW crate::livehost (the run_pulse_loop production caller), harness-endpoint mirror of shellwake::reconcile_once:
- Manifest-from-disk via registry::resolve_option_in(&info.adapter) — no harness dep; BrainLifecycle::with_config gates on [session.psyche_init].
- spawn_live_host thread: boot + 5s-tick reconcile, wired into brainproc::run_brain beside spawn_wake_host (replaces the 175-182 no-op; resume_sessions PTY-delivery axis left intact + comment-clarified as orthogonal).
- FULL set-diff reconcile_once: un-host (tabled ∧ ¬online → trip held stop flag + join + de-table), host (online ∧ ¬tabled → spawn_psyche + driver thread), leave-alone. ONLINE-ONLY, daemon-authoritative.
- Staged behind interim: status-absent interim perches ignored → no double-host. W0.1 scan dormant in prod this wave.

UNITS a–e ([unit->REQ-DAEMON-1], all green):
  (a) reconcile_hosts_an_online_live_endpoint_and_drives_its_pulse — hosts it AND proves the driver loop really ticks (polls last_active_ms gets stamped, not just thread-spawned)
  (b) reconcile_skips_an_offline_live_endpoint — entry guard
  (c) reconcile_does_not_double_host — contains-guard
  (d) fresh_brain_rehydrates_an_online_live_endpoint — drop the table (sim brain restart) → fresh LiveSet re-hosts the still-online perch
  (e) reconcile_unhosts_on_offline_transition — exit guard, driver stopped + de-tabled (no forever-pulsing dead agent)

GATES: 293 spt-daemon lib units + daemon_e2e green; clippy --workspace --all-targets -D warnings clean (pre-flighted per the lesson); traceable EXIT 0. REQ-DAEMON-1 impl+unit ADDED; int re-point stays W0.4 (real-brain prod path — NOT regressed to type-level), first-host establish-marks-online stays W0.2/W0.3.

Reconcile-diff completeness (incl un-host) is in (e); the real-brain W0.4 arc is next-but-one. Awaiting your W0.1 impl gate (+ confirm both-runner CI green on PR #16). — todlando
EOF
echo sent=$?

Submit W0.1 to doyle for impl gate


➜ SENT:doyle
sent=0


W0.1 submitted to doyle for the impl gate. Push triggered fresh PR #16 CI — poll for green to relay to doyle (alongside his verdict via Monitor).


ScheduleWakeup

Polling PR #16 CI on the W0.1 push (1448a0a) for both-runner green to relay to doyle; his W0.1 impl-gate verdict also arrives via Monitor. CI ~5-8min, harness can't notify on it.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 07:21:00 (in 267s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


**W0.1 DONE — committed @1448a0a, pushed, submitted to doyle.**

Built `crate::livehost` (the `run_pulse_loop` production caller) exactly to doyle's gated design — harness-endpoint mirror of `shellwake::reconcile_once`:
- **Manifest-from-disk** via `resolve_option_in(&info.adapter)` — no harness dep
- **Two orthogonal axes**: `resume_sessions` (PTY delivery) untouched; Psyche endpoint-table via `list_self_perch_ids`
- **ONLINE-ONLY full set-diff** reconcile (boot+tick): host / un-host / leave-alone; daemon-authoritative; staged behind the interim (no double-host)
- Wired into `run_brain` beside `spawn_wake_host`

Units a–e all green (incl pulse-loop-really-ticks proof + the un-host exit-guard). 293 lib units + daemon_e2e green; clippy `--workspace` clean; traceable EXIT 0. int re-point + first-host correctly deferred to W0.4/W0.2-3 (no over-claim).

Awaiting doyle's impl gate + PR #16 CI green. Polling.


<task-notification>
<task-id>bmby1p3hm</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/8" id="39e15414" type="msg" from="doyle">[doyle → todlando] W0.1 IMPL GATE — CONDITIONAL PASS. Design conformance verified by READING the code (not the report), one CI condition + one hardening note.

VERIFIED vs the gated design:
- livehost::reconcile_once = full set-diff, STOP-SIDE FIRST: un-host (tabled ∧ ¬online → trip stop flag + join + </EVENT-PART>
<EVENT-PART seq="2/8" id="39e15414">de-table), host (online ∧ ¬tabled → spawn_psyche + run_pulse_loop driver), leave-alone. ONLINE-ONLY, daemon-authoritative (CONTEXT:194). ✓
- Manifest-from-disk via resolve_option_in(&amp;info.adapter), gated on [session.psyche_init]. No harness dep. ✓
- PRODUCTION WIRING CONFIRMED: spawn_live_host wired into brainproc::run_brain</EVENT-PART>
<EVENT-PART seq="3/8" id="39e15414"> (line 230, beside spawn_wake_host) — THE run_pulse_loop production caller that was the 175-182 no-op now EXISTS. That's the whole point of W0.1; it's there. ✓
- Staged behind interim (status-absent exclusion → no double-host); W0.1 scan dormant in prod this wave. ✓
- int tag NOT regressed — stays daemon_e2e (type-level); re-po</EVENT-PART>
<EVENT-PART seq="4/8" id="39e15414">int to the real-brain prod path correctly deferred to W0.4. No new false-green introduced. ✓
- Units a-e present, entry(b)/exit(e) distinct, (a) proves the loop TICKS (last_active_ms stamped) not just spawns — that's the right proof. ✓

CONDITION (before full PASS): both-runner CI green on the W0.1 commit 1448a0a. The run list s</EVENT-PART>
<EVENT-PART seq="5/8" id="39e15414">till only shows the W1 run (06cf6f1) — no 1448a0a run registered yet. Ping me the 1448a0a run id when both runners + traceability are green; I confirm independently → W0.1 full PASS → you're clear for W0.2.

HARDENING NOTE (W0.4, not a W0.1 blocker): your un-host triggers on `!still_online`, which is ALSO true on a transient read_i</EVENT-PART>
<EVENT-PART seq="6/8" id="39e15414">nfo FAILURE (perch momentarily unreadable) → stop_host → next tick re-hosts → Psyche stop+respawn churn on a disk blip. The reconcile self-heals (re-hosts next tick), so it's not wrong, but a respawn-on-transient is disruptive. Consider distinguishing definitive-offline (read ok, status≠online) from read-error (don't un-host on a read </EVENT-PART>
<EVENT-PART seq="7/8" id="39e15414">failure — leave the driver, retry next tick). Fold into W0.4 hardening; flag it in the plan so it's not lost.

So: design+impl PASS on read; W0.1 closes on 1448a0a CI-green. Good clean build to the design. Proceed to W0.2 scoping (the atomic first-host cutover — I gate that interim-off ⟺ brain-first-host-on has no per-endpoint gap)</EVENT-PART>
<EVENT-PART seq="8/8" id="39e15414">.</EVENT-PART></event>
</task-notification>