Edit

use std::path::Path;

use spt_store::info;

use std::path::Path;

use spt_store::info;
use spt_store::perch::{self, ParentHint};


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\resting.rs has been updated successfully. (file state is current in your context — no need to Read it back)

use std::path::Path;

use spt_store::info;
use spt_store::perch::{self, ParentHint};

/// The resting states (CONTEXT §dormant/suspended): the registry's

/// `Status::Offline` is deliberately NOT here — offline means *node


/// One instance's durable resting record.

/// Arm the endpoint's echo-gate sentinel — the **cross-process transition
/// echo trigger** (REQ-INST-4). The daemon's feed points (the attach arm's
/// detach, the registry arm's attention shift, `spt suspend`) hold no
/// manifest runtime, so they cannot run the bounded summarizer call
/// themselves; arming the gate hands the echo to the endpoint's **own**
/// pulse loop ([`spt_live::pulse::take_echo_gate`] — read-and-clear, fires
/// exactly once), which owns the runtime, history, and commune dir.
///
/// This is also the KH 7.4 posture **by construction**: the daemon never
/// hosts another agent's bounded LLM call from a shared loop — N agents'
/// transition echoes are N gate files consumed by N per-agent pulse loops,
/// so one slow summarizer cannot stall another agent's edge
/// (REQ-HAZARD-DAEMON-SCHED-NONBLOCKING stays unbound until the daemon
/// hosts per-agent runtimes — the same 7.4 fan-out seam the in-pump
/// reconcile turn waits on).
// [impl->REQ-INST-4]
pub fn arm_transition_echo(id: &str) {
    let gate = perch::resolve_echo_gate_file(id, ParentHint::Infer);
    if let Some(parent) = gate.parent() {
        let _ = std::fs::create_dir_all(parent);
    }
    let _ = std::fs::write(&gate, b"");
}

/// The wake-edge catch-up effects (best-effort, loud-on-surface like every
/// notif path): resurface undismissed notifs at the Wake boundary (the D8
/// deferral — real now that the edge exists) and drop the freshness-pull
/// marker the peer pump consumes.
// [impl->REQ-INST-3]
pub fn fire_wake_effects(id: &str) {
    if let Ok(store) = spt_store::notif::NotifStore::open() {
        let policy = crate::notif::NotifSurfacePolicy::load();
        let _ = crate::notif::resurface_at_boundary(
            &store,
            id,
            &policy,
            &perch::owlery_dir(),
            now_ms(),
            crate::notif::SUPPRESSION_WINDOW_MS,
        );
    }
    let marker = perch::identity_dir().join(PULL_MARKER_FILE);
    let _ = request_freshness_pull(&marker);
}

/// The daemon-side transition host for feed points that own no manifest
/// runtime (the attach arm, the registry arm's attention shift, the
/// `spt suspend`/`spt wake` CLI): [`apply_event`] with the echo seam bound
/// to the gate ([`arm_transition_echo`] — the endpoint's own pulse loop
/// fires the real commune) and the wake seam to [`fire_wake_effects`].
/// Resolves the canonical perch for `id`. A lifecycle host that *does* own
/// the runtime uses `BrainLifecycle::rest_event` instead (inline echo).
// [impl->REQ-INST-3]
pub fn daemon_rest_event(
    id: &str,
    event: RestEvent,
    node_auto_suspend_ms: Option<u64>,
) -> Result<Option<EdgeReport>, String> {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    apply_event(
        &perch_path,
        event,
        node_auto_suspend_ms,
        now_ms(),
        || {
            arm_transition_echo(id);
            Ok(())
        },
        || fire_wake_effects(id),
    )
}

fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

/// One instance's durable resting record.


/// Arm the endpoint's echo-gate sentinel — the **cross-process transition
/// echo trigger** (REQ-INST-4). The daemon's feed points (the attach arm's
/// detach, the registry arm's attention shift, `spt suspend`) hold no
/// manifest runtime, so they cannot run the bounded summarizer call
/// themselves; arming the gate hands the echo to the endpoint's **own**
/// pulse loop ([`spt_live::pulse::take_echo_gate`] — read-and-clear, fires
/// exactly once), which owns the runtime, history, and commune dir.
///
/// This is also the KH 7.4 posture **by construction**: the daemon never
/// hosts another agent's bounded LLM call from a shared loop — N agents'
/// transition echoes are N gate files consumed by N per-agent pulse loops,
/// so one slow summarizer cannot stall another agent's edge
/// (REQ-HAZARD-DAEMON-SCHED-NONBLOCKING stays unbound until the daemon
/// hosts per-agent runtimes — the same 7.4 fan-out seam the in-pump
/// reconcile turn waits on).
// [impl->REQ-INST-4]
pub fn arm_transition_echo(id: &str) {
    let gate = perch::resolve_echo_gate_file(id, ParentHint::Infer);
    if let Some(parent) = gate.parent() {
        let _ = std::fs::create_dir_all(parent);
    }
    let _ = std::fs::write(&gate, b"");
}

/// The wake-edge catch-up effects (best-effort, loud-on-surface like every
/// notif path): resurface undismissed notifs at the Wake boundary (the D8
/// deferral — real now that the edge exists) and drop the freshness-pull
/// marker the peer pump consumes.
// [impl->REQ-INST-3]
pub fn fire_wake_effects(id: &str) {
    if let Ok(store) = spt_store::notif::NotifStore::open() {
        let policy = crate::notif::NotifSurfacePolicy::load();
        let _ = crate::notif::resurface_at_boundary(
            &store,
            id,
            &policy,
            &perch::owlery_dir(),
            now_ms(),
            crate::notif::SUPPRESSION_WINDOW_MS,
        );
    }
    let marker = perch::identity_dir().join(PULL_MARKER_FILE);
    let _ = request_freshness_pull(&marker);
}

/// The daemon-side transition host for feed points that own no manifest
/// runtime (the attach arm, the registry arm's attention shift, the
/// `spt suspend`/`spt wake` CLI): [`apply_event`] with the echo seam bound
/// to the gate ([`arm_transition_echo`] — the endpoint's own pulse loop
/// fires the real commune) and the wake seam to [`fire_wake_effects`].
/// Resolves the canonical perch for `id`. A lifecycle host that *does* own
/// the runtime uses `BrainLifecycle::rest_event` instead (inline echo).
// [impl->REQ-INST-3]
pub fn daemon_rest_event(
    id: &str,
    event: RestEvent,
    node_auto_suspend_ms: Option<u64>,
) -> Result<Option<EdgeReport>, String> {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    apply_event(
        &perch_path,
        event,
        node_auto_suspend_ms,
        now_ms(),
        || {
            arm_transition_echo(id);
            Ok(())
        },
        || fire_wake_effects(id),
    )
}

fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

/// One instance's durable resting record.

Ok(Some(EdgeReport { from, to, echo_fired }))

+/// Arm the endpoint's echo-gate sentinel — the **cross-process transition

+/// echo trigger** (REQ-INST-4). The daemon's feed points (the attach arm's

+/// detach, the registry arm's attention shift, `spt suspend`) hold no

+/// manifest runtime, so they cannot run the bounded summarizer call

+/// themselves; arming the gate hands the echo to the endpoint's **own**

+/// pulse loop ([`spt_live::pulse::take_echo_gate`] — read-and-clear, fires

+/// exactly once), which owns the runtime, history, and commune dir.

+/// This is also the KH 7.4 posture **by construction**: the daemon never

+/// hosts another agent's bounded LLM call from a shared loop — N agents'

+/// transition echoes are N gate files consumed by N per-agent pulse loops,

+/// so one slow summarizer cannot stall another agent's edge

+/// (REQ-HAZARD-DAEMON-SCHED-NONBLOCKING stays unbound until the daemon

+/// hosts per-agent runtimes — the same 7.4 fan-out seam the in-pump

+    let gate = perch::resolve_echo_gate_file(id, ParentHint::Infer);

+    if let Some(parent) = gate.parent() {

+        let _ = std::fs::create_dir_all(parent);

+    let _ = std::fs::write(&gate, b"");

+/// The wake-edge catch-up effects (best-effort, loud-on-surface like every

+/// notif path): resurface undismissed notifs at the Wake boundary (the D8

+/// deferral — real now that the edge exists) and drop the freshness-pull

+    if let Ok(store) = spt_store::notif::NotifStore::open() {

+        let policy = crate::notif::NotifSurfacePolicy::load();

+        let _ = crate::notif::resurface_at_boundary(

+            crate::notif::SUPPRESSION_WINDOW_MS,

+    let marker = perch::identity_dir().join(PULL_MARKER_FILE);

+    let _ = request_freshness_pull(&marker);

+/// The daemon-side transition host for feed points that own no manifest

+/// runtime (the attach arm, the registry arm's attention shift, the

+/// `spt suspend`/`spt wake` CLI): [`apply_event`] with the echo seam bound

+/// to the gate ([`arm_transition_echo`] — the endpoint's own pulse loop

+/// fires the real commune) and the wake seam to [`fire_wake_effects`].

+/// Resolves the canonical perch for `id`. A lifecycle host that *does* own

+/// the runtime uses `BrainLifecycle::rest_event` instead (inline echo).

+) -> Result<Option<EdgeReport>, String> {

+    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);

+        .duration_since(std::time::UNIX_EPOCH)

#[derive(Debug, Clone, Copy, PartialEq, Eq)]


**B. Lifecycle delegates wake effects + pulse gating:**


    /// The wake-edge effects (best-effort, loud-on-surface like every notif
    /// path): resurface undismissed notifs at the Wake boundary and drop the
    /// freshness-pull marker for the peer pump.
    // [impl->REQ-INST-3]
    fn fire_wake_effects(&self) {
        if let Ok(store) = spt_store::notif::NotifStore::open() {
            let policy = crate::notif::NotifSurfacePolicy::load();
            let _ = crate::notif::resurface_at_boundary(
                &store,
                &self.id,
                &policy,
                &perch::owlery_dir(),
                now_ms(),
                crate::notif::SUPPRESSION_WINDOW_MS,
            );
        }
        let marker =
            spt_store::perch::identity_dir().join(crate::resting::PULL_MARKER_FILE);
        let _ = crate::resting::request_freshness_pull(&marker);
    }

    /// The wake-edge effects — the shared daemon glue
    /// ([`crate::resting::fire_wake_effects`]): resurface at the Wake
    /// boundary + the freshness-pull marker for the peer pump.
    // [impl->REQ-INST-3]
    fn fire_wake_effects(&self) {
        crate::resting::fire_wake_effects(&self.id);
    }


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\lifecycle.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    /// The wake-edge effects (best-effort, loud-on-surface like every notif
    /// path): resurface undismissed notifs at the Wake boundary and drop the
    /// freshness-pull marker for the peer pump.
    // [impl->REQ-INST-3]
    fn fire_wake_effects(&self) {
        if let Ok(store) = spt_store::notif::NotifStore::open() {
            let policy = crate::notif::NotifSurfacePolicy::load();
            let _ = crate::notif::resurface_at_boundary(
                &store,
                &self.id,
                &policy,
                &perch::owlery_dir(),
                now_ms(),
                crate::notif::SUPPRESSION_WINDOW_MS,
            );
        }
        let marker =
            spt_store::perch::identity_dir().join(crate::resting::PULL_MARKER_FILE);
        let _ = crate::resting::request_freshness_pull(&marker);
    }

    /// The wake-edge effects — the shared daemon glue
    /// ([`crate::resting::fire_wake_effects`]): resurface at the Wake
    /// boundary + the freshness-pull marker for the peer pump.
    // [impl->REQ-INST-3]
    fn fire_wake_effects(&self) {
        crate::resting::fire_wake_effects(&self.id);
    }

-    /// The wake-edge effects (best-effort, loud-on-surface like every notif

-    /// path): resurface undismissed notifs at the Wake boundary and drop the

-    /// freshness-pull marker for the peer pump.

+    /// The wake-edge effects — the shared daemon glue

+    /// ([`crate::resting::fire_wake_effects`]): resurface at the Wake

+    /// boundary + the freshness-pull marker for the peer pump.

-        if let Ok(store) = spt_store::notif::NotifStore::open() {

-            let policy = crate::notif::NotifSurfacePolicy::load();

-            let _ = crate::notif::resurface_at_boundary(

-                crate::notif::SUPPRESSION_WINDOW_MS,

-            spt_store::perch::identity_dir().join(crate::resting::PULL_MARKER_FILE);

-        let _ = crate::resting::request_freshness_pull(&marker);

+        crate::resting::fire_wake_effects(&self.id);

/// Run the echo-commune into the commune dir (T3). No-op (Ok) without a dir.


    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops
    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one
    /// echo-commune (T3).
    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {
        self.touch_active();
        let now = now_ms();

    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops
    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one
    /// echo-commune (T3).
    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {
        let now = now_ms();
        // Recency follows the resting model (D9-2 — the D8 gap closed): only
        // an ACTIVE (or pre-D9-2 recordless) instance refreshes
        // `last_active_ms` — a resting seat ticking its pulse must never win
        // a "most-recently-active" resolution (REQ-UPD-4's consent target,
        // the notif first-fire). A dormant seat instead checks the opt-in
        // auto-suspend threshold (counted from its dormancy-onset anchor);
        // a suspended seat just keeps ingesting.
        // [impl->REQ-INST-3]
        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);
        match crate::resting::read_rest(&perch_path).map(|r| r.state) {
            None | Some(crate::resting::RestState::Active) => self.touch_active(),
            Some(crate::resting::RestState::Dormant) => {
                let _ = self.rest_event(
                    session_id,
                    crate::resting::RestEvent::AutoSuspendTick { now_ms: now },
                );
            }
            Some(crate::resting::RestState::Suspended) => {}
        }


    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops
    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one
    /// echo-commune (T3).
    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {
        self.touch_active();
        let now = now_ms();

    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops
    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one
    /// echo-commune (T3).
    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {
        let now = now_ms();
        // Recency follows the resting model (D9-2 — the D8 gap closed): only
        // an ACTIVE (or pre-D9-2 recordless) instance refreshes
        // `last_active_ms` — a resting seat ticking its pulse must never win
        // a "most-recently-active" resolution (REQ-UPD-4's consent target,
        // the notif first-fire). A dormant seat instead checks the opt-in
        // auto-suspend threshold (counted from its dormancy-onset anchor);
        // a suspended seat just keeps ingesting.
        // [impl->REQ-INST-3]
        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);
        match crate::resting::read_rest(&perch_path).map(|r| r.state) {
            None | Some(crate::resting::RestState::Active) => self.touch_active(),
            Some(crate::resting::RestState::Dormant) => {
                let _ = self.rest_event(
                    session_id,
                    crate::resting::RestEvent::AutoSuspendTick { now_ms: now },
                );
            }
            Some(crate::resting::RestState::Suspended) => {}
        }

+        // Recency follows the resting model (D9-2 — the D8 gap closed): only

+        // an ACTIVE (or pre-D9-2 recordless) instance refreshes

+        // `last_active_ms` — a resting seat ticking its pulse must never win

+        // a "most-recently-active" resolution (REQ-UPD-4's consent target,

+        // the notif first-fire). A dormant seat instead checks the opt-in

+        // auto-suspend threshold (counted from its dormancy-onset anchor);

+        // a suspended seat just keeps ingesting.

+        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);

+        match crate::resting::read_rest(&perch_path).map(|r| r.state) {

+            None | Some(crate::resting::RestState::Active) => self.touch_active(),

+            Some(crate::resting::RestState::Dormant) => {

+                let _ = self.rest_event(

+                    crate::resting::RestEvent::AutoSuspendTick { now_ms: now },

+            Some(crate::resting::RestState::Suspended) => {}


**C. RegistryHost flip detection:**


    /// Apply one inbound feed under the gate. `origin_node` MUST be the
    /// handshake-proven stream-table origin, never payload bytes.
    // [impl->REQ-INST-7]
    // [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
    pub fn apply_feed(
        &self,
        origin_node: &str,
        updates: &[RegistryUpdate],
        policy: &RegistryGatePolicy,
    ) -> Vec<RegistryApplyVerdict> {
        let mut regs = self.regs.lock().unwrap();
        let verdicts = updates
            .iter()
            .map(|u| {
                if !policy.subnets.iter().any(|s| s == &u.subnet) {
                    return RegistryApplyVerdict::NotMember;
                }
                if !policy.trust.is_trusted(&u.subnet, origin_node) {
                    return RegistryApplyVerdict::UntrustedOrigin;
                }
                let reg = regs.entry(u.subnet.clone()).or_default();
                // The epoch lease (4.8) decides; an own-row echo from a peer
                // is dropped Stale by construction (our epoch is newer or
                // equal — never authored elsewhere).
                RegistryApplyVerdict::Merged(
                    reg.merge_instance(&u.endpoint_id, u.instance.clone()),
                )
            })
            .collect();
        drop(regs);
        self.write_snapshots();
        verdicts
    }

    /// Apply one inbound feed under the gate. `origin_node` MUST be the
    /// handshake-proven stream-table origin, never payload bytes.
    // [impl->REQ-INST-7]
    // [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
    pub fn apply_feed(
        &self,
        origin_node: &str,
        updates: &[RegistryUpdate],
        policy: &RegistryGatePolicy,
    ) -> Vec<RegistryApplyVerdict> {
        self.apply_feed_flips(origin_node, updates, policy).0
    }

    /// [`apply_feed`](Self::apply_feed) that ALSO reports **attention
    /// flips** (D9-2, REQ-INST-3): the endpoint ids whose admitted update was
    /// an observed *transition to `Active` on a peer node* — the wire signal
    /// that another instance of the id just became the driven one (its
    /// daemon advertises transitions epoch-bumped), which rests a local
    /// instance of the same id warm (CONTEXT: driving `ling@laptop` makes
    /// `ling@desktop` dormant).
    ///
    /// Flip = `Updated` merge where the stored row's status was NOT `Active`
    /// and the incoming one is. Deliberately **never `Inserted`**: this
    /// registry is in-memory, so a daemon restart re-inserts every peer row
    /// at its *current* status — counting inserts would knock local
    /// instances dormant on every restart; and never the periodic
    /// re-advertisement (`Updated` with prev already `Active` is not a
    /// flip). The lease has already dropped stale/equal epochs before any
    /// of this runs.
    // [impl->REQ-INST-3]
    pub fn apply_feed_flips(
        &self,
        origin_node: &str,
        updates: &[RegistryUpdate],
        policy: &RegistryGatePolicy,
    ) -> (Vec<RegistryApplyVerdict>, Vec<String>) {
        let mut regs = self.regs.lock().unwrap();
        let mut flips: Vec<String> = Vec::new();
        let verdicts = updates
            .iter()
            .map(|u| {
                if !policy.subnets.iter().any(|s| s == &u.subnet) {
                    return RegistryApplyVerdict::NotMember;
                }
                if !policy.trust.is_trusted(&u.subnet, origin_node) {
                    return RegistryApplyVerdict::UntrustedOrigin;
                }
                let reg = regs.entry(u.subnet.clone()).or_default();
                let prev = reg
                    .instances(&u.endpoint_id)
                    .iter()
                    .find(|i| i.node == u.instance.node)
                    .map(|i| i.status);
                // The epoch lease (4.8) decides; an own-row echo from a peer
                // is dropped Stale by construction (our epoch is newer or
                // equal — never authored elsewhere).
                let outcome = reg.merge_instance(&u.endpoint_id, u.instance.clone());
                if outcome == MergeOutcome::Updated
                    && u.instance.status == Status::Active
                    && prev != Some(Status::Active)
                    && u.instance.node != self.node_hex
                    && !flips.contains(&u.endpoint_id)
                {
                    flips.push(u.endpoint_id.clone());
                }
                RegistryApplyVerdict::Merged(outcome)
            })
            .collect();
        drop(regs);
        self.write_snapshots();
        (verdicts, flips)
    }


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    /// Apply one inbound feed under the gate. `origin_node` MUST be the
    /// handshake-proven stream-table origin, never payload bytes.
    // [impl->REQ-INST-7]
    // [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
    pub fn apply_feed(
        &self,
        origin_node: &str,
        updates: &[RegistryUpdate],
        policy: &RegistryGatePolicy,
    ) -> Vec<RegistryApplyVerdict> {
        let mut regs = self.regs.lock().unwrap();
        let verdicts = updates
            .iter()
            .map(|u| {
                if !policy.subnets.iter().any(|s| s == &u.subnet) {
                    return RegistryApplyVerdict::NotMember;
                }
                if !policy.trust.is_trusted(&u.subnet, origin_node) {
                    return RegistryApplyVerdict::UntrustedOrigin;
                }
                let reg = regs.entry(u.subnet.clone()).or_default();
                // The epoch lease (4.8) decides; an own-row echo from a peer
                // is dropped Stale by construction (our epoch is newer or
                // equal — never authored elsewhere).
                RegistryApplyVerdict::Merged(
                    reg.merge_instance(&u.endpoint_id, u.instance.clone()),
                )
            })
            .collect();
        drop(regs);
        self.write_snapshots();
        verdicts
    }

    /// Apply one inbound feed under the gate. `origin_node` MUST be the
    /// handshake-proven stream-table origin, never payload bytes.
    // [impl->REQ-INST-7]
    // [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
    pub fn apply_feed(
        &self,
        origin_node: &str,
        updates: &[RegistryUpdate],
        policy: &RegistryGatePolicy,
    ) -> Vec<RegistryApplyVerdict> {
        self.apply_feed_flips(origin_node, updates, policy).0
    }

    /// [`apply_feed`](Self::apply_feed) that ALSO reports **attention
    /// flips** (D9-2, REQ-INST-3): the endpoint ids whose admitted update was
    /// an observed *transition to `Active` on a peer node* — the wire signal
    /// that another instance of the id just became the driven one (its
    /// daemon advertises transitions epoch-bumped), which rests a local
    /// instance of the same id warm (CONTEXT: driving `ling@laptop` makes
    /// `ling@desktop` dormant).
    ///
    /// Flip = `Updated` merge where the stored row's status was NOT `Active`
    /// and the incoming one is. Deliberately **never `Inserted`**: this
    /// registry is in-memory, so a daemon restart re-inserts every peer row
    /// at its *current* status — counting inserts would knock local
    /// instances dormant on every restart; and never the periodic
    /// re-advertisement (`Updated` with prev already `Active` is not a
    /// flip). The lease has already dropped stale/equal epochs before any
    /// of this runs.
    // [impl->REQ-INST-3]
    pub fn apply_feed_flips(
        &self,
        origin_node: &str,
        updates: &[RegistryUpdate],
        policy: &RegistryGatePolicy,
    ) -> (Vec<RegistryApplyVerdict>, Vec<String>) {
        let mut regs = self.regs.lock().unwrap();
        let mut flips: Vec<String> = Vec::new();
        let verdicts = updates
            .iter()
            .map(|u| {
                if !policy.subnets.iter().any(|s| s == &u.subnet) {
                    return RegistryApplyVerdict::NotMember;
                }
                if !policy.trust.is_trusted(&u.subnet, origin_node) {
                    return RegistryApplyVerdict::UntrustedOrigin;
                }
                let reg = regs.entry(u.subnet.clone()).or_default();
                let prev = reg
                    .instances(&u.endpoint_id)
                    .iter()
                    .find(|i| i.node == u.instance.node)
                    .map(|i| i.status);
                // The epoch lease (4.8) decides; an own-row echo from a peer
                // is dropped Stale by construction (our epoch is newer or
                // equal — never authored elsewhere).
                let outcome = reg.merge_instance(&u.endpoint_id, u.instance.clone());
                if outcome == MergeOutcome::Updated
                    && u.instance.status == Status::Active
                    && prev != Some(Status::Active)
                    && u.instance.node != self.node_hex
                    && !flips.contains(&u.endpoint_id)
                {
                    flips.push(u.endpoint_id.clone());
                }
                RegistryApplyVerdict::Merged(outcome)
            })
            .collect();
        drop(regs);
        self.write_snapshots();
        (verdicts, flips)
    }

+        self.apply_feed_flips(origin_node, updates, policy).0

+    /// [`apply_feed`](Self::apply_feed) that ALSO reports **attention

+    /// flips** (D9-2, REQ-INST-3): the endpoint ids whose admitted update was

+    /// an observed *transition to `Active` on a peer node* — the wire signal

+    /// that another instance of the id just became the driven one (its

+    /// daemon advertises transitions epoch-bumped), which rests a local

+    /// instance of the same id warm (CONTEXT: driving `ling@laptop` makes

+    /// Flip = `Updated` merge where the stored row's status was NOT `Active`

+    /// and the incoming one is. Deliberately **never `Inserted`**: this

+    /// registry is in-memory, so a daemon restart re-inserts every peer row

+    /// at its *current* status — counting inserts would knock local

+    /// instances dormant on every restart; and never the periodic

+    /// re-advertisement (`Updated` with prev already `Active` is not a

+    /// flip). The lease has already dropped stale/equal epochs before any

+    ) -> (Vec<RegistryApplyVerdict>, Vec<String>) {

+        let mut flips: Vec<String> = Vec::new();

+                    .instances(&u.endpoint_id)

+                    .find(|i| i.node == u.instance.node)

+                    .map(|i| i.status);

-                RegistryApplyVerdict::Merged(

-                    reg.merge_instance(&u.endpoint_id, u.instance.clone()),

+                let outcome = reg.merge_instance(&u.endpoint_id, u.instance.clone());

+                if outcome == MergeOutcome::Updated

+                    && u.instance.status == Status::Active

+                    && prev != Some(Status::Active)

+                    && u.instance.node != self.node_hex

+                    && !flips.contains(&u.endpoint_id)

+                    flips.push(u.endpoint_id.clone());

+                RegistryApplyVerdict::Merged(outcome)

/// Scan the owlery and (re-)advertise every self perch into every member


D9-2-1: resting state machine + Status::Suspended + advertise-follows-state

Pure RestState/RestEvent transition core in spt-daemon/resting.rs; info.json rest_state + dormant_since_ms + auto_suspend_after_ms; DaemonConfig auto_suspend_after_ms knob; registry Status::Suspended (additive serde, routable); advertise_local maps rest_state → Status. Activate REQ-INST-3 [impl, unit]. CI green.

D9-2-2: transition effects — echo edge + wake resurface + freshness pull

Transition host applies events, persists state, fires effects: run_echo_commune on active→(dormant|suspended) edge (KH 3.3, REQ-INST-4 [impl, unit]); wake edge fires resurface_at_boundary + writes freshness-pull marker the peer pump consumes (forces due_sync). KH 7.4: if >1 agent's echo scheduled by daemon, activate REQ-HAZARD-DAEMON-SCHED-NONBLOCKING + off-scheduler echo. CI green.

D9-2-3: production feed points + spt suspend/wake CLI + INST-6 seam doc

Attach arm feeds Wake on Request / Detach on viewport end; registry inbound arm feeds AttentionShift on peer row transition-to-Active for locally hosted id; spt suspend/spt wake local CLI arms; REQ-INST-6 deferred-message gate seam documented (stays inactive). CI green; plan status updated.


Updated task #3 status


// [unit->REQ-INST-3] registry advertisement FOLLOWS the resting-state

    // [unit->REQ-INST-3] attention-flip detection: only an OBSERVED
    // transition to Active on a peer node reports a flip — never the first
    // insert (a restarted daemon's re-learned rows must not knock local
    // instances dormant), never a periodic Active re-advertisement, never a
    // stale-epoch row, and never this node's own rows.
    #[test]
    fn apply_feed_flips_only_on_observed_transition_to_active() {
        let dir = tempfile::tempdir().unwrap();
        let h = host(dir.path()); // node_hex = "aa11"
        let p = policy("home", "bb22");

        fn upd_with(subnet: &str, id: &str, node: &str, status: Status, epoch: u64) -> RegistryUpdate {
            RegistryUpdate {
                subnet: subnet.to_string(),
                endpoint_id: id.to_string(),
                instance: Instance { node: node.to_string(), status, epoch },
            }
        }

        // First sight of the row (Inserted, Active): NOT a flip.
        let (_, flips) =
            h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Active, 1)], &p);
        assert!(flips.is_empty(), "insert is not a flip (restart re-learn must not fire)");

        // Periodic re-advertisement (Updated, prev already Active): NOT a flip.
        let (_, flips) =
            h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Active, 2)], &p);
        assert!(flips.is_empty(), "Active→Active re-advertisement is not a flip");

        // The peer instance rests…
        let (_, flips) =
            h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Dormant, 3)], &p);
        assert!(flips.is_empty(), "going dormant is not an attention flip");

        // …and WAKES: Dormant→Active at a fresh epoch — THE flip.
        let (_, flips) =
            h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Active, 4)], &p);
        assert_eq!(flips, vec!["ling".to_string()], "observed transition to Active flips");

        // A lagging stale wake (lower epoch) is dropped by the lease — no flip.
        h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Dormant, 5)], &p);
        let (v, flips) =
            h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Active, 4)], &p);
        assert_eq!(v, vec![RegistryApplyVerdict::Merged(MergeOutcome::Stale)]);
        assert!(flips.is_empty(), "the lease drops it before flip detection");
    }

    // [unit->REQ-INST-3] registry advertisement FOLLOWS the resting-state


    // [unit->REQ-INST-3] attention-flip detection: only an OBSERVED
    // transition to Active on a peer node reports a flip — never the first
    // insert (a restarted daemon's re-learned rows must not knock local
    // instances dormant), never a periodic Active re-advertisement, never a
    // stale-epoch row, and never this node's own rows.
    #[test]
    fn apply_feed_flips_only_on_observed_transition_to_active() {
        let dir = tempfile::tempdir().unwrap();
        let h = host(dir.path()); // node_hex = "aa11"
        let p = policy("home", "bb22");

        fn upd_with(subnet: &str, id: &str, node: &str, status: Status, epoch: u64) -> RegistryUpdate {
            RegistryUpdate {
                subnet: subnet.to_string(),
                endpoint_id: id.to_string(),
                instance: Instance { node: node.to_string(), status, epoch },
            }
        }

        // First sight of the row (Inserted, Active): NOT a flip.
        let (_, flips) =
            h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Active, 1)], &p);
        assert!(flips.is_empty(), "insert is not a flip (restart re-learn must not fire)");

        // Periodic re-advertisement (Updated, prev already Active): NOT a flip.
        let (_, flips) =
            h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Active, 2)], &p);
        assert!(flips.is_empty(), "Active→Active re-advertisement is not a flip");

        // The peer instance rests…
        let (_, flips) =
            h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Dormant, 3)], &p);
        assert!(flips.is_empty(), "going dormant is not an attention flip");

        // …and WAKES: Dormant→Active at a fresh epoch — THE flip.
        let (_, flips) =
            h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Active, 4)], &p);
        assert_eq!(flips, vec!["ling".to_string()], "observed transition to Active flips");

        // A lagging stale wake (lower epoch) is dropped by the lease — no flip.
        h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Dormant, 5)], &p);
        let (v, flips) =
            h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Active, 4)], &p);
        assert_eq!(v, vec![RegistryApplyVerdict::Merged(MergeOutcome::Stale)]);
        assert!(flips.is_empty(), "the lease drops it before flip detection");
    }

    // [unit->REQ-INST-3] registry advertisement FOLLOWS the resting-state

+    // [unit->REQ-INST-3] attention-flip detection: only an OBSERVED

+    // transition to Active on a peer node reports a flip — never the first

+    // insert (a restarted daemon's re-learned rows must not knock local

+    // instances dormant), never a periodic Active re-advertisement, never a

+    // stale-epoch row, and never this node's own rows.

+    fn apply_feed_flips_only_on_observed_transition_to_active() {

+        let dir = tempfile::tempdir().unwrap();

+        let h = host(dir.path()); // node_hex = "aa11"

+        let p = policy("home", "bb22");

+        fn upd_with(subnet: &str, id: &str, node: &str, status: Status, epoch: u64) -> RegistryUpdate {

+                subnet: subnet.to_string(),

+                endpoint_id: id.to_string(),

+                instance: Instance { node: node.to_string(), status, epoch },

+        // First sight of the row (Inserted, Active): NOT a flip.

+            h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Active, 1)], &p);

+        assert!(flips.is_empty(), "insert is not a flip (restart re-learn must not fire)");

+        // Periodic re-advertisement (Updated, prev already Active): NOT a flip.

+            h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Active, 2)], &p);

+        assert!(flips.is_empty(), "Active→Active re-advertisement is not a flip");

+            h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Dormant, 3)], &p);

+        assert!(flips.is_empty(), "going dormant is not an attention flip");

+        // …and WAKES: Dormant→Active at a fresh epoch — THE flip.

+            h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Active, 4)], &p);

+        assert_eq!(flips, vec!["ling".to_string()], "observed transition to Active flips");

+        // A lagging stale wake (lower epoch) is dropped by the lease — no flip.

+        h.apply_feed_flips("bb22", &[upd_with("home", "ling", "bb22", Status::Dormant, 5)], &p);

+        assert_eq!(v, vec![RegistryApplyVerdict::Merged(MergeOutcome::Stale)]);

+        assert!(flips.is_empty(), "the lease drops it before flip detection");

// machine: a live perch advertises its machine state (active → dormant →

// suspended), each transition reaching the registry as an epoch-bumped


**D. Dispatch registry arm feeds AttentionShift:**


/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into
/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧
/// origin trusted in that subnet), until the peer finishes.
// [impl->REQ-INST-7]
fn serve_registry_feed(
    brain: &mut Brain,
    stream_id: u64,
    origin: &str,
    registry: &RegistryHost,
) -> DispatchOutcome {
    let policy = RegistryGatePolicy::load();
    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {
        return DispatchOutcome::Failed(e.to_string());
    }
    let mut decoder = RegistryDecoder::new();
    let mut merged = 0usize;
    loop {
        match brain.read_event() {
            Ok(BrokerEvent::NetStreamData { stream_id: sid, bytes, .. }) if sid == stream_id => {
                let updates = decoder.push(&bytes);
                if updates.is_empty() {
                    continue;
                }
                merged += registry.apply_feed(origin, &updates, &policy).len();
            }

/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into
/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧
/// origin trusted in that subnet), until the peer finishes.
///
/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer
/// instance of an id observed transitioning to `Active` — its node's daemon
/// just gave it a driver, so a *locally hosted* instance of the same id
/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`
/// dormant). The rest edge's transition echo is gate-armed
/// ([`crate::resting::arm_transition_echo`]) — the endpoint's own pulse
/// loop runs the bounded summarizer call, never this worker (the KH 7.4
/// non-blocking posture: N flips in one feed are N gate files, not N LLM
/// calls serialized here).
// [impl->REQ-INST-7]
fn serve_registry_feed(
    brain: &mut Brain,
    stream_id: u64,
    origin: &str,
    registry: &RegistryHost,
) -> DispatchOutcome {
    let policy = RegistryGatePolicy::load();
    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {
        return DispatchOutcome::Failed(e.to_string());
    }
    let mut decoder = RegistryDecoder::new();
    let mut merged = 0usize;
    loop {
        match brain.read_event() {
            Ok(BrokerEvent::NetStreamData { stream_id: sid, bytes, .. }) if sid == stream_id => {
                let updates = decoder.push(&bytes);
                if updates.is_empty() {
                    continue;
                }
                let (verdicts, flips) = registry.apply_feed_flips(origin, &updates, &policy);
                merged += verdicts.len();
                // Attention shift (D9-2): only ids this node actually hosts
                // (a perch with a record); everything else is a no-op here.
                // [impl->REQ-INST-3]
                for id in flips {
                    let local = perch::resolve_perch_path(&id, spt_store::perch::ParentHint::Infer);
                    if local.join("info.json").exists() {
                        if let Err(e) = crate::resting::daemon_rest_event(
                            &id,
                            crate::resting::RestEvent::AttentionShift,
                            None,
                        ) {
                            eprintln!("REST_SHIFT_FAIL:{id}:{e}");
                        }
                    }
                }
            }


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\dispatch.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into
/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧
/// origin trusted in that subnet), until the peer finishes.
// [impl->REQ-INST-7]
fn serve_registry_feed(
    brain: &mut Brain,
    stream_id: u64,
    origin: &str,
    registry: &RegistryHost,
) -> DispatchOutcome {
    let policy = RegistryGatePolicy::load();
    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {
        return DispatchOutcome::Failed(e.to_string());
    }
    let mut decoder = RegistryDecoder::new();
    let mut merged = 0usize;
    loop {
        match brain.read_event() {
            Ok(BrokerEvent::NetStreamData { stream_id: sid, bytes, .. }) if sid == stream_id => {
                let updates = decoder.push(&bytes);
                if updates.is_empty() {
                    continue;
                }
                merged += registry.apply_feed(origin, &updates, &policy).len();
            }

/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into
/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧
/// origin trusted in that subnet), until the peer finishes.
///
/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer
/// instance of an id observed transitioning to `Active` — its node's daemon
/// just gave it a driver, so a *locally hosted* instance of the same id
/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`
/// dormant). The rest edge's transition echo is gate-armed
/// ([`crate::resting::arm_transition_echo`]) — the endpoint's own pulse
/// loop runs the bounded summarizer call, never this worker (the KH 7.4
/// non-blocking posture: N flips in one feed are N gate files, not N LLM
/// calls serialized here).
// [impl->REQ-INST-7]
fn serve_registry_feed(
    brain: &mut Brain,
    stream_id: u64,
    origin: &str,
    registry: &RegistryHost,
) -> DispatchOutcome {
    let policy = RegistryGatePolicy::load();
    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {
        return DispatchOutcome::Failed(e.to_string());
    }
    let mut decoder = RegistryDecoder::new();
    let mut merged = 0usize;
    loop {
        match brain.read_event() {
            Ok(BrokerEvent::NetStreamData { stream_id: sid, bytes, .. }) if sid == stream_id => {
                let updates = decoder.push(&bytes);
                if updates.is_empty() {
                    continue;
                }
                let (verdicts, flips) = registry.apply_feed_flips(origin, &updates, &policy);
                merged += verdicts.len();
                // Attention shift (D9-2): only ids this node actually hosts
                // (a perch with a record); everything else is a no-op here.
                // [impl->REQ-INST-3]
                for id in flips {
                    let local = perch::resolve_perch_path(&id, spt_store::perch::ParentHint::Infer);
                    if local.join("info.json").exists() {
                        if let Err(e) = crate::resting::daemon_rest_event(
                            &id,
                            crate::resting::RestEvent::AttentionShift,
                            None,
                        ) {
                            eprintln!("REST_SHIFT_FAIL:{id}:{e}");
                        }
                    }
                }
            }

+/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer

+/// instance of an id observed transitioning to `Active` — its node's daemon

+/// just gave it a driver, so a *locally hosted* instance of the same id

+/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`

+/// dormant). The rest edge's transition echo is gate-armed

+/// ([`crate::resting::arm_transition_echo`]) — the endpoint's own pulse

+/// loop runs the bounded summarizer call, never this worker (the KH 7.4

+/// non-blocking posture: N flips in one feed are N gate files, not N LLM

-                merged += registry.apply_feed(origin, &updates, &policy).len();

+                let (verdicts, flips) = registry.apply_feed_flips(origin, &updates, &policy);

+                merged += verdicts.len();

+                // Attention shift (D9-2): only ids this node actually hosts

+                // (a perch with a record); everything else is a no-op here.

+                    let local = perch::resolve_perch_path(&id, spt_store::perch::ParentHint::Infer);

+                    if local.join("info.json").exists() {

+                        if let Err(e) = crate::resting::daemon_rest_event(

+                            crate::resting::RestEvent::AttentionShift,

+                            eprintln!("REST_SHIFT_FAIL:{id}:{e}");

Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {

return DispatchOutcome::Served(format!("registry feed: {merged} record(s)"));


**E. Attach arm feeds Wake/Detach:**


fn mark_driven_by(endpoint: &str, origin_node: Option<&str>) {
    let perch = resolve_perch_path(endpoint, ParentHint::Infer);
    let _ = spt_store::info::set_driven_by(&perch, origin_node);
}

fn mark_driven_by(endpoint: &str, origin_node: Option<&str>) {
    let perch = resolve_perch_path(endpoint, ParentHint::Infer);
    let _ = spt_store::info::set_driven_by(&perch, origin_node);
}

/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a
/// served viewport is a **driver** — attaching wakes the endpoint, the
/// viewport ending detaches it (active → dormant; the transition echo is
/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like
/// [`mark_driven_by`]: a session served without a perch has no resting
/// record to move.
// [impl->REQ-INST-3]
fn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {
    let _ = crate::resting::daemon_rest_event(endpoint, event, None);
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\attach.rs has been updated successfully. (file state is current in your context — no need to Read it back)

fn mark_driven_by(endpoint: &str, origin_node: Option<&str>) {
    let perch = resolve_perch_path(endpoint, ParentHint::Infer);
    let _ = spt_store::info::set_driven_by(&perch, origin_node);
}

/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a
/// served viewport is a **driver** — attaching wakes the endpoint, the
/// viewport ending detaches it (active → dormant; the transition echo is
/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like
/// [`mark_driven_by`]: a session served without a perch has no resting
/// record to move.
// [impl->REQ-INST-3]
fn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {
    let _ = crate::resting::daemon_rest_event(endpoint, event, None);
}

+/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a

+/// served viewport is a **driver** — attaching wakes the endpoint, the

+/// viewport ending detaches it (active → dormant; the transition echo is

+/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like

+/// [`mark_driven_by`]: a session served without a perch has no resting

+fn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {

+    let _ = crate::resting::daemon_rest_event(endpoint, event, None);


                            // brain crash (the same replayed Request that
                            // re-attaches); cleared when the viewport ends.
                            // [impl->REQ-REACH-1]
                            mark_driven_by(endpoint, Some(origin_node));

                            // brain crash (the same replayed Request that
                            // re-attaches); cleared when the viewport ends.
                            // [impl->REQ-REACH-1]
                            mark_driven_by(endpoint, Some(origin_node));
                            // A driver attached — the wake edge (re-activate
                            // in place; idempotent on an already-active seat).
                            feed_rest(endpoint, crate::resting::RestEvent::Wake);


                            // brain crash (the same replayed Request that
                            // re-attaches); cleared when the viewport ends.
                            // [impl->REQ-REACH-1]
                            mark_driven_by(endpoint, Some(origin_node));
                            // A driver attached — the wake edge (re-activate
                            // in place; idempotent on an already-active seat).
                            feed_rest(endpoint, crate::resting::RestEvent::Wake);

+                            // A driver attached — the wake edge (re-activate

+                            // in place; idempotent on an already-active seat).

+                            feed_rest(endpoint, crate::resting::RestEvent::Wake);

AttachRecord::Input { data_b64, op_id } if attached => {


            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {
                brain.net_stream_send(stream_id, &[], None, true)?;
                if attached {
                    mark_driven_by(endpoint, None);
                }
                return Ok(AttachServeOutcome::Detached);
            }

            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {
                brain.net_stream_send(stream_id, &[], None, true)?;
                if attached {
                    mark_driven_by(endpoint, None);
                    // The driver detached — the endpoint rests warm.
                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
                }
                return Ok(AttachServeOutcome::Detached);
            }


            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {
                brain.net_stream_send(stream_id, &[], None, true)?;
                if attached {
                    mark_driven_by(endpoint, None);
                }
                return Ok(AttachServeOutcome::Detached);
            }

            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {
                brain.net_stream_send(stream_id, &[], None, true)?;
                if attached {
                    mark_driven_by(endpoint, None);
                    // The driver detached — the endpoint rests warm.
                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
                }
                return Ok(AttachServeOutcome::Detached);
            }

+                    // The driver detached — the endpoint rests warm.

+                    feed_rest(endpoint, crate::resting::RestEvent::Detach);


            BrokerEvent::Exit { code, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Exit { code });
                brain.net_stream_send(stream_id, &line, None, true)?;
                mark_driven_by(endpoint, None);
                return Ok(AttachServeOutcome::Exited(code));
            }

            BrokerEvent::Error { message } => {
                if attached {
                    mark_driven_by(endpoint, None);
                }
                return Err(io::Error::other(message));
            }

            BrokerEvent::Exit { code, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Exit { code });
                brain.net_stream_send(stream_id, &line, None, true)?;
                mark_driven_by(endpoint, None);
                feed_rest(endpoint, crate::resting::RestEvent::Detach);
                return Ok(AttachServeOutcome::Exited(code));
            }

            BrokerEvent::Error { message } => {
                if attached {
                    mark_driven_by(endpoint, None);
                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
                }
                return Err(io::Error::other(message));
            }


            BrokerEvent::Exit { code, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Exit { code });
                brain.net_stream_send(stream_id, &line, None, true)?;
                mark_driven_by(endpoint, None);
                return Ok(AttachServeOutcome::Exited(code));
            }

            BrokerEvent::Error { message } => {
                if attached {
                    mark_driven_by(endpoint, None);
                }
                return Err(io::Error::other(message));
            }

            BrokerEvent::Exit { code, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Exit { code });
                brain.net_stream_send(stream_id, &line, None, true)?;
                mark_driven_by(endpoint, None);
                feed_rest(endpoint, crate::resting::RestEvent::Detach);
                return Ok(AttachServeOutcome::Exited(code));
            }

            BrokerEvent::Error { message } => {
                if attached {
                    mark_driven_by(endpoint, None);
                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
                }
                return Err(io::Error::other(message));
            }

+                feed_rest(endpoint, crate::resting::RestEvent::Detach);


**F. CLI — `spt suspend` / `spt wake`:**


    /// Harness-contract inbound surface (REQ-API-1): the entry points a
    /// harness's hooks fire to mutate on-disk SPT state.
    Api(crate::api::ApiArgs),

    /// Rest a local endpoint cold (REQ-INST-3): the resting-state machine's
    /// suspend edge. From dormant — or straight from active, in which case
    /// the transition echo commune still fires (gate-armed for the
    /// endpoint's own pulse loop). Local addressing only — the qualified
    /// `id@node` suspend-from-anywhere arm is M5.
    Suspend {
        /// The (local) endpoint id to suspend.
        id: String,
    },
    /// Wake a resting local endpoint in place (REQ-INST-3): re-activates the
    /// existing seat (state's already there — no fresh spawn), resurfaces
    /// undismissed notifs at the Wake boundary, and requests an immediate
    /// context freshness pull from trusted peers. Local addressing only.
    Wake {
        /// The (local) endpoint id to wake.
        id: String,
    },
    /// Harness-contract inbound surface (REQ-API-1): the entry points a
    /// harness's hooks fire to mutate on-disk SPT state.
    Api(crate::api::ApiArgs),


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    /// Rest a local endpoint cold (REQ-INST-3): the resting-state machine's
    /// suspend edge. From dormant — or straight from active, in which case
    /// the transition echo commune still fires (gate-armed for the
    /// endpoint's own pulse loop). Local addressing only — the qualified
    /// `id@node` suspend-from-anywhere arm is M5.
    Suspend {
        /// The (local) endpoint id to suspend.
        id: String,
    },
    /// Wake a resting local endpoint in place (REQ-INST-3): re-activates the
    /// existing seat (state's already there — no fresh spawn), resurfaces
    /// undismissed notifs at the Wake boundary, and requests an immediate
    /// context freshness pull from trusted peers. Local addressing only.
    Wake {
        /// The (local) endpoint id to wake.
        id: String,
    },
    /// Harness-contract inbound surface (REQ-API-1): the entry points a
    /// harness's hooks fire to mutate on-disk SPT state.
    Api(crate::api::ApiArgs),

+    /// Rest a local endpoint cold (REQ-INST-3): the resting-state machine's

+    /// suspend edge. From dormant — or straight from active, in which case

+    /// the transition echo commune still fires (gate-armed for the

+    /// endpoint's own pulse loop). Local addressing only — the qualified

+    /// `id@node` suspend-from-anywhere arm is M5.

+        /// The (local) endpoint id to suspend.

+    /// Wake a resting local endpoint in place (REQ-INST-3): re-activates the

+    /// existing seat (state's already there — no fresh spawn), resurfaces

+    /// undismissed notifs at the Wake boundary, and requests an immediate

+    /// context freshness pull from trusted peers. Local addressing only.

+        /// The (local) endpoint id to wake.


        Cmd::Digest { id, follow, json } => cmd_digest(&id, follow, json),
        Cmd::Rename { old_id, new_id } => cmd_rename(&old_id, &new_id),

        Cmd::Digest { id, follow, json } => cmd_digest(&id, follow, json),
        Cmd::Suspend { id } => cmd_rest(&id, spt_daemon::RestEvent::Suspend, "SUSPENDED"),
        Cmd::Wake { id } => cmd_rest(&id, spt_daemon::RestEvent::Wake, "WOKE"),
        Cmd::Rename { old_id, new_id } => cmd_rename(&old_id, &new_id),


        Cmd::Digest { id, follow, json } => cmd_digest(&id, follow, json),
        Cmd::Rename { old_id, new_id } => cmd_rename(&old_id, &new_id),

        Cmd::Digest { id, follow, json } => cmd_digest(&id, follow, json),
        Cmd::Suspend { id } => cmd_rest(&id, spt_daemon::RestEvent::Suspend, "SUSPENDED"),
        Cmd::Wake { id } => cmd_rest(&id, spt_daemon::RestEvent::Wake, "WOKE"),
        Cmd::Rename { old_id, new_id } => cmd_rename(&old_id, &new_id),

NotifCmd::Dismiss { notif_id } => cmd_notif_dismiss(&notif_id),

+        Cmd::Suspend { id } => cmd_rest(&id, spt_daemon::RestEvent::Suspend, "SUSPENDED"),

+        Cmd::Wake { id } => cmd_rest(&id, spt_daemon::RestEvent::Wake, "WOKE"),


/// Run the per-machine daemon (the hidden `spt daemon` entry point). Blocks

/// `spt suspend <id>` / `spt wake <id>` — the manual resting-state edges
/// (REQ-INST-3): apply the event against the local endpoint's durable record
/// through the daemon-side transition host (echo gate-armed for the
/// endpoint's own pulse loop; wake fires the boundary resurface + freshness
/// pull). Local addressing only — the qualified `id@node`
/// suspend-from-anywhere arm composes with the M5 remote machinery.
// [impl->REQ-INST-3]
fn cmd_rest(id: &str, event: spt_daemon::RestEvent, verb: &str) -> i32 {
    if id.contains('@') || id.contains(':') {
        eprintln!(
            "CROSS_NODE_M5:{id} — qualified [subnet:]id@node resting commands are the M5 \
             remote arm; local ids only"
        );
        return 1;
    }
    let knob = spt_daemon::DaemonConfig::load().auto_suspend_after_ms;
    match spt_daemon::daemon_rest_event(id, event, knob) {
        Ok(Some(report)) => {
            eprintln!("{verb}:{id} ({:?} -> {:?})", report.from, report.to);
            0
        }
        Ok(None) => {
            // Idempotent UX: the machine reports no edge (already there).
            eprintln!("NO_EDGE:{id} (already in the target state)");
            0
        }
        Err(e) => {
            eprintln!("{verb}_FAIL:{id}: {e}");
            1
        }
    }
}

/// Run the per-machine daemon (the hidden `spt daemon` entry point). Blocks


/// `spt suspend <id>` / `spt wake <id>` — the manual resting-state edges
/// (REQ-INST-3): apply the event against the local endpoint's durable record
/// through the daemon-side transition host (echo gate-armed for the
/// endpoint's own pulse loop; wake fires the boundary resurface + freshness
/// pull). Local addressing only — the qualified `id@node`
/// suspend-from-anywhere arm composes with the M5 remote machinery.
// [impl->REQ-INST-3]
fn cmd_rest(id: &str, event: spt_daemon::RestEvent, verb: &str) -> i32 {
    if id.contains('@') || id.contains(':') {
        eprintln!(
            "CROSS_NODE_M5:{id} — qualified [subnet:]id@node resting commands are the M5 \
             remote arm; local ids only"
        );
        return 1;
    }
    let knob = spt_daemon::DaemonConfig::load().auto_suspend_after_ms;
    match spt_daemon::daemon_rest_event(id, event, knob) {
        Ok(Some(report)) => {
            eprintln!("{verb}:{id} ({:?} -> {:?})", report.from, report.to);
            0
        }
        Ok(None) => {
            // Idempotent UX: the machine reports no edge (already there).
            eprintln!("NO_EDGE:{id} (already in the target state)");
            0
        }
        Err(e) => {
            eprintln!("{verb}_FAIL:{id}: {e}");
            1
        }
    }
}

/// Run the per-machine daemon (the hidden `spt daemon` entry point). Blocks

+/// `spt suspend <id>` / `spt wake <id>` — the manual resting-state edges

+/// (REQ-INST-3): apply the event against the local endpoint's durable record

+/// through the daemon-side transition host (echo gate-armed for the

+/// endpoint's own pulse loop; wake fires the boundary resurface + freshness

+/// pull). Local addressing only — the qualified `id@node`

+/// suspend-from-anywhere arm composes with the M5 remote machinery.

+fn cmd_rest(id: &str, event: spt_daemon::RestEvent, verb: &str) -> i32 {

+    if id.contains('@') || id.contains(':') {

+            "CROSS_NODE_M5:{id} — qualified [subnet:]id@node resting commands are the M5 \

+             remote arm; local ids only"

+    let knob = spt_daemon::DaemonConfig::load().auto_suspend_after_ms;

+    match spt_daemon::daemon_rest_event(id, event, knob) {

+            eprintln!("{verb}:{id} ({:?} -> {:?})", report.from, report.to);

+            // Idempotent UX: the machine reports no edge (already there).

+            eprintln!("NO_EDGE:{id} (already in the target state)");

+            eprintln!("{verb}_FAIL:{id}: {e}");

/// serving the broker + seed-control channels until killed; a bind contention

/// (another daemon already owns this home) surfaces as a non-zero exit.


        // The digest surface + its flags.
        assert!(matches!(parse(&["spt", "digest", "doyle"]).unwrap().cmd, Cmd::Digest { .. }));

        // [unit->REQ-INST-3] the resting-command surfaces parse (one local id).
        assert!(matches!(parse(&["spt", "suspend", "ling"]).unwrap().cmd, Cmd::Suspend { .. }));
        assert!(matches!(parse(&["spt", "wake", "ling"]).unwrap().cmd, Cmd::Wake { .. }));
        assert!(parse(&["spt", "suspend"]).is_err(), "id required");
        assert!(parse(&["spt", "wake"]).is_err(), "id required");
        // The digest surface + its flags.
        assert!(matches!(parse(&["spt", "digest", "doyle"]).unwrap().cmd, Cmd::Digest { .. }));


        // [unit->REQ-INST-3] the resting-command surfaces parse (one local id).
        assert!(matches!(parse(&["spt", "suspend", "ling"]).unwrap().cmd, Cmd::Suspend { .. }));
        assert!(matches!(parse(&["spt", "wake", "ling"]).unwrap().cmd, Cmd::Wake { .. }));
        assert!(parse(&["spt", "suspend"]).is_err(), "id required");
        assert!(parse(&["spt", "wake"]).is_err(), "id required");
        // The digest surface + its flags.
        assert!(matches!(parse(&["spt", "digest", "doyle"]).unwrap().cmd, Cmd::Digest { .. }));

// [unit->REQ-INST-11] the rename surface parses (two positional ids).

assert!(matches!(parse(&["spt", "rename", "ling", "oak"]).unwrap().cmd, Cmd::Rename { .. }));

assert!(parse(&["spt", "rename", "ling"]).is_err(), "new id required");

+        // [unit->REQ-INST-3] the resting-command surfaces parse (one local id).

+        assert!(matches!(parse(&["spt", "suspend", "ling"]).unwrap().cmd, Cmd::Suspend { .. }));

+        assert!(matches!(parse(&["spt", "wake", "ling"]).unwrap().cmd, Cmd::Wake { .. }));

+        assert!(parse(&["spt", "suspend"]).is_err(), "id required");

+        assert!(parse(&["spt", "wake"]).is_err(), "id required");


Exports + remaining tests + seam docs:


pub use resting::{
    apply_event, effective_auto_suspend, read_rest, request_freshness_pull, take_freshness_pull,
    transition, write_rest, EdgeReport, RestEvent, RestRecord, RestState, PULL_MARKER_FILE,
};

pub use resting::{
    apply_event, arm_transition_echo, daemon_rest_event, effective_auto_suspend,
    fire_wake_effects, read_rest, request_freshness_pull, take_freshness_pull, transition,
    write_rest, EdgeReport, RestEvent, RestRecord, RestState, PULL_MARKER_FILE,
};


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

pub use resting::{
    apply_event, arm_transition_echo, daemon_rest_event, effective_auto_suspend,
    fire_wake_effects, read_rest, request_freshness_pull, take_freshness_pull, transition,
    write_rest, EdgeReport, RestEvent, RestRecord, RestState, PULL_MARKER_FILE,
};

//! `spt-daemon` — the per-machine broker/brain supervisor (M3b).
//!
//! ADR-0004 splits the logical daemon into two implementation layers so a
//! routine self-update swaps the logic without disturbing live endpoints:
//!
//! - a **broker** (stable kernel) holds *only* the un-transferable resources a
//!   live consumer would lose continuity on if the logic restarted — PTY
//!   masters (from [`spt_term`]), spawned harness children, accepted/listening
//!   sockets — and runs no logic;
//! - a **brain** (restartable logic) holds routing, registry, manifest parse,
//!   and the re-hosted [`spt_live`] lifecycle, attaching to the broker over a
//!   **versioned local IPC** so a *newer brain talks to an older broker*.
//!
//! **B0 (this commit)** lands the crate's layer position plus that versioned
//! IPC seam — the load-bearing contract everything else rides:
//!
//! - [`frame`] — the forward-compatible [`Envelope`] schema + the version
//!   [`Hello`] handshake (KNOWN-HAZARDS 2.3, [`REQ-HAZARD-HANDOFF-ARGV-COMPAT`]);
//! - [`codec`] — length-prefixed JSON framing over any [`std::io::Read`]/`Write`;
//! - [`transport`] — the OS-split carrier (named pipe / UDS) behind one
//!   [`DaemonTransport`] trait, mirroring `spt-term`'s `SessionSurface`.
//!
//! **B1 (this commit)** adds the **broker kernel** ([`broker`]) — the stable
//! half that hosts a real `spt-term` PTY child and relays its output/input over
//! the IPC, holding only the ADR-0004 §B resources and no logic — plus the
//! broker↔brain message catalog ([`msg`]).
//!
//! **B3 (this commit)** consolidates the interim no-daemon model into the brain:
//! the `api listen` pulse / Psyche / drop-ingest loops become scheduled
//! in-process loops ([`lifecycle::BrainLifecycle`]) paced by a durable,
//! configurable period ([`config::DaemonConfig`]) instead of the interim 5s
//! constant; the only residue left in the harness-hosted session is the thin,
//! stateless [`relay::Relay`]. The `spt` binary's `LiveHost` becomes a shim over
//! `BrainLifecycle` (re-host, not rewrite) ([`REQ-DAEMON-1`]).
//!
//! **B5 (this commit)** makes the broker↔brain boundary **exactly-once across a
//! brain crash** ([`effect::EffectJournal`]): an idempotent `input` carries a
//! durable `op_id`, and the broker dedups at the effect keyed by that id over a
//! broker-owned recovery anchor (KNOWN-HAZARDS 7.2, [`REQ-HAZARD-RESTART-IDEMPOTENT`];
//! Spike #6's two constraints — broker-owned anchor, dedup-at-effect-by-id —
//! instantiated on the real PTY-write boundary).
//!
//! **C0 (this commit)** opens M3c — the self-update engine ([`update`]): the
//! ADR-0004 §A update-class taxonomy ([`UpdateClass`] + [`classify`]) and the
//! **brain-only zero-interruption swap** ([`apply_brain_only`], [`REQ-UPD-3`]).
//! The brain-only apply *triggers* a handoff over the B2 substrate — snapshot →
//! drop → re-attach — so the live PTY child + its output stream survive the logic
//! swap untouched; it never re-implements the handoff and never touches a broker
//! resource.
//!
//! **C1 (this commit)** adds the verify-before-handoff gate ([`release`]): a
//! release is signature-verified ([`REQ-UPD-2`]) and hardened against rollback
//! ([`REQ-HAZARD-UPDATE-ROLLBACK`]) — Ed25519 over signed metadata, key
//! trust/revocation, channel pinning, metadata expiry, monotonic version, and a
//! SHA-256 binding the signature to the actual binary bytes. [`plan_verified`] is
//! the front door: it produces an [`UpdatePlan`] *only* for a release that passes
//! every check, so an unverified binary can never reach [`apply_brain_only`].
//!
//! **C3 (this commit)** conducts the **adapter ripple** ([`adapter_update`]):
//! after self-update, spt-core updates each registered adapter via its manifest
//! `[update]` avenue ([`REQ-UPD-5`], [`REQ-SEAM-UPDATE`]). `file_pull` payloads
//! are verified against the **adapter's own** key (manifest-declared) + SHA-256
//! (adapter content signing, ADR-0004 §D); `delegated` updaters are opaque, so
//! spt-core delegates the trust only when the adapter attests `self_verifies`,
//! else skips them as unverifiable.
//!
//! **D4b (this commit)** makes broker-held QUIC streams survive brain restarts
//! **gaplessly and exactly-once** (Spike #3 invariants A–D as real tests): each
//! stream's reads are buffered in a sequenced ring (the B2 `OutputLog`
//! discipline) a brain subscribes to with a resume cursor; sends/opens carry
//! durable `op_id`s journaled at the broker, so a restarted brain's re-drive
//! dedups instead of double-writing the wire.
//!
//! **D4a (this commit)** moves the node's network presence into the broker
//! ([`nethost::NetHost`], ADR-0004 §B): a dedicated tokio runtime hosts the
//! iroh endpoint + every accepted/dialed QUIC connection, so a brain restart
//! never interrupts a cross-node connection. The brain drives net operations
//! (endpoint-status, dial) over new IPC frames in the same forward-compatible
//! catalog; an `op_id`-carrying dial is exactly-once through the B5
//! [`effect::EffectJournal`]. The endpoint binds as the durable node identity
//! (`spt-store`'s `node.key`), with the relay stance from `daemon.json`.
//!
//! **D5a (this commit)** puts WAN message delivery on the D4 substrate: a
//! [`WanMessage`](spt_net::net::wanmsg::WanMessage) NDJSON feed over a
//! broker-owned QUIC stream funnels through [`wan::receive_wan`] — the ADR-0009
//! access-gate seam ([`access`], handshake-proven origin only, KNOWN-HAZARDS
//! 7.5) plus durable op-id dedup (7.2, wire-fed) — into the existing local
//! REQ-MSG delivery path.
//!
//! **D5b (this commit)** adds remote terminal attach ([`attach`], REQ-INST-8):
//! a byte-stream viewport onto a local session for an operator daemon on
//! another node — session output pumped as sequenced
//! [`AttachRecord`](spt_net::net::attach::AttachRecord)s (deduped at the
//! operator's render cursor), operator input through the B5 exactly-once PTY
//! path, the whole loop gated and restart-survivable purely from the D4/B5
//! machinery.
//!
//! The brain + gapless handoff (B2) and the rest of the M3b task table build on
//! this seam. `spt-daemon` is internal supervisor machinery, **not** public SDK
//! (R-ARCH-2).
//!
//! [`Envelope`]: frame::Envelope
//! [`Hello`]: frame::Hello
//! [`DaemonTransport`]: transport::DaemonTransport

pub mod access;
pub mod adapter_update;
pub mod attach;
pub mod brain;
pub mod broker;
pub mod codec;
pub mod config;
pub mod consent;
pub mod daemon;
pub mod digest;
pub mod digesthub;
pub mod dispatch;
pub mod effect;
pub mod endpoint;
pub mod frame;
pub mod lifecycle;
pub mod msg;
pub mod nethost;
pub mod notif;
pub mod notifsync;
pub mod peerloop;
pub mod propagate;
pub mod psyrelay;
pub mod reconcile;
pub mod registryhost;
pub mod relay;
pub mod relcache;
pub mod release;
pub mod resting;
pub mod seedmap;
pub mod sync;
pub mod transport;
pub mod update;
pub mod wan;
pub mod xfer;

#[cfg(test)]
mod test_home;

pub use access::{access_check, classify_inbound, note_outbound, AccessDecision, InboundClass};
pub use attach::{request_attach, send_attach_input, serve_attach, AttachServeOutcome};
pub use adapter_update::{
    conduct_ripple, plan_adapter_update, AdapterRipple, AdapterUpdateOutcome, FilePullPayload,
    SkipReason,
};
pub use brain::{Brain, BrainState, BrokerEvent};
pub use broker::Broker;
pub use config::{DaemonConfig, RelayChoice};
pub use consent::{decide as decide_consent, most_recently_active, ConsentDecision, ConsentTarget};
pub use daemon::{ensure_running, is_running, Daemon};
pub use digest::{ingest_chunk, DigestEngine, DigestUpdate};
pub use digesthub::{
    digest_to_json, follow, pull_snapshot, render_digest, render_update, serve_digest_control,
    update_to_json, DigestHub,
};
pub use effect::{EffectJournal, EffectKey, EffectKind, Outcome};
pub use endpoint::{broker_socket_name, daemon_pid_path, digest_socket_name, seed_socket_name};
pub use lifecycle::{BrainLifecycle, TickReport};
pub use nethost::{NetConfig, NetHost, NET_EFFECT_SESSION};
pub use notif::{
    first_fire, most_recently_active_visible, produce_and_first_fire, produce_consent_notif,
    resurface_at_boundary, FirstFireOutcome, NotifSurfacePolicy, ResurfaceOutcome,
    NOTIF_KIND_AGENT, NOTIF_KIND_CONSENT, NOTIF_KIND_PSYCHE, SUPPRESSION_WINDOW_MS,
};
pub use notifsync::{apply_notif_feed, emit_notif_feed, NotifApplyVerdict, NotifPolicy};
pub use propagate::{request_update, serve_update, UpdatePullOutcome, UpdateServeOutcome};
pub use psyrelay::{
    psyche_turn_and_relay, relay_psyche_outbound, PsycheNotifCtx, RelayOutcome, PSYCHE_BODY_CAP,
};
pub use relay::Relay;
pub use relcache::ReleaseCache;
pub use resting::{
    apply_event, effective_auto_suspend, read_rest, request_freshness_pull, take_freshness_pull,
    transition, write_rest, EdgeReport, RestEvent, RestRecord, RestState, PULL_MARKER_FILE,
};
pub use seedmap::{put_seed, take_seed, SeedRegistry};
pub use frame::{
    accept_hello, Envelope, HandshakeError, Hello, Role, IPC_PROTOCOL_VERSION,
    MIN_COMPATIBLE_VERSION,
};
pub use transport::{DaemonTransport, LocalSocketTransport};
pub use release::{
    parse_verifying_key, sha256_hex, verify_artifact, verify_metadata, verify_signature,
    RejectReason, ReleaseMetadata, SignedRelease, VerifyPolicy,
};
pub use update::{
    apply_brain_only, classify, plan_update, plan_verified, ApplyError, BrokerAbi, ReleaseSpec,
    UpdateClass, UpdatePlan, BROKER_RESOURCE_ABI,
};
pub use sync::{
    reconcile_after_sync, request_sync, select_refs, serve_sync, ReconcileWiring, SyncPolicy,
    SyncPullReport, SyncServeOutcome,
};
pub use xfer::{fetch_file, push_file, serve_xfer, XferServeOutcome};
pub use wan::{receive_wan, WanOutcome};

-    apply_event, effective_auto_suspend, read_rest, request_freshness_pull, take_freshness_pull,

-    transition, write_rest, EdgeReport, RestEvent, RestRecord, RestState, PULL_MARKER_FILE,

+    apply_event, arm_transition_echo, daemon_rest_event, effective_auto_suspend,

+    fire_wake_effects, read_rest, request_freshness_pull, take_freshness_pull, transition,

+    write_rest, EdgeReport, RestEvent, RestRecord, RestState, PULL_MARKER_FILE,


// [unit->REQ-INST-3] the freshness-pull marker: dropped idempotently,

    // [unit->REQ-INST-4] the daemon-side host arms the endpoint's echo gate
    // at the rest edge (the cross-process trigger — the endpoint's OWN pulse
    // loop runs the bounded summarizer call, never the daemon's feed worker:
    // the KH 7.4 non-blocking posture by construction) and fires no gate on
    // the wake edge.
    #[test]
    fn daemon_rest_event_arms_gate_at_rest_edge_only() {
        crate::test_home::with_home(|_| {
            let perch_path = perch::resolve_perch_path("ling", ParentHint::Infer);
            std::fs::create_dir_all(&perch_path).unwrap();
            write_info(&perch_path, &InfoJson::new("ling", "t", 4242, "sid", "live_agent"))
                .unwrap();
            let gate = perch::resolve_echo_gate_file("ling", ParentHint::Infer);
            assert!(!gate.exists());

            // active → dormant: the gate is armed for ling's pulse loop.
            let report = daemon_rest_event("ling", RestEvent::Detach, None)
                .expect("apply ok")
                .expect("edge");
            assert!(report.echo_fired, "the rest edge fired the (gate-armed) echo seam");
            assert!(gate.exists(), "gate armed — ling's own pulse fires the real commune");
            assert_eq!(read_rest(&perch_path).unwrap().state, RestState::Dormant);

            // wake: no gate re-arm; the freshness-pull marker drops instead.
            std::fs::remove_file(&gate).unwrap();
            let report = daemon_rest_event("ling", RestEvent::Wake, None)
                .expect("apply ok")
                .expect("edge");
            assert_eq!(report.to, RestState::Active);
            assert!(!gate.exists(), "wake arms no echo");
            assert!(
                perch::identity_dir().join(PULL_MARKER_FILE).exists(),
                "wake dropped the freshness-pull marker"
            );
        });
    }

    // [unit->REQ-INST-3] the freshness-pull marker: dropped idempotently,


    // [unit->REQ-INST-4] the daemon-side host arms the endpoint's echo gate
    // at the rest edge (the cross-process trigger — the endpoint's OWN pulse
    // loop runs the bounded summarizer call, never the daemon's feed worker:
    // the KH 7.4 non-blocking posture by construction) and fires no gate on
    // the wake edge.
    #[test]
    fn daemon_rest_event_arms_gate_at_rest_edge_only() {
        crate::test_home::with_home(|_| {
            let perch_path = perch::resolve_perch_path("ling", ParentHint::Infer);
            std::fs::create_dir_all(&perch_path).unwrap();
            write_info(&perch_path, &InfoJson::new("ling", "t", 4242, "sid", "live_agent"))
                .unwrap();
            let gate = perch::resolve_echo_gate_file("ling", ParentHint::Infer);
            assert!(!gate.exists());

            // active → dormant: the gate is armed for ling's pulse loop.
            let report = daemon_rest_event("ling", RestEvent::Detach, None)
                .expect("apply ok")
                .expect("edge");
            assert!(report.echo_fired, "the rest edge fired the (gate-armed) echo seam");
            assert!(gate.exists(), "gate armed — ling's own pulse fires the real commune");
            assert_eq!(read_rest(&perch_path).unwrap().state, RestState::Dormant);

            // wake: no gate re-arm; the freshness-pull marker drops instead.
            std::fs::remove_file(&gate).unwrap();
            let report = daemon_rest_event("ling", RestEvent::Wake, None)
                .expect("apply ok")
                .expect("edge");
            assert_eq!(report.to, RestState::Active);
            assert!(!gate.exists(), "wake arms no echo");
            assert!(
                perch::identity_dir().join(PULL_MARKER_FILE).exists(),
                "wake dropped the freshness-pull marker"
            );
        });
    }

    // [unit->REQ-INST-3] the freshness-pull marker: dropped idempotently,

assert_eq!(out, None, "endpoint 0 = explicit OFF beats the node knob");

+    // [unit->REQ-INST-4] the daemon-side host arms the endpoint's echo gate

+    // at the rest edge (the cross-process trigger — the endpoint's OWN pulse

+    // loop runs the bounded summarizer call, never the daemon's feed worker:

+    // the KH 7.4 non-blocking posture by construction) and fires no gate on

+    fn daemon_rest_event_arms_gate_at_rest_edge_only() {

+        crate::test_home::with_home(|_| {

+            let perch_path = perch::resolve_perch_path("ling", ParentHint::Infer);

+            std::fs::create_dir_all(&perch_path).unwrap();

+            write_info(&perch_path, &InfoJson::new("ling", "t", 4242, "sid", "live_agent"))

+            let gate = perch::resolve_echo_gate_file("ling", ParentHint::Infer);

+            // active → dormant: the gate is armed for ling's pulse loop.

+            let report = daemon_rest_event("ling", RestEvent::Detach, None)

+            assert!(report.echo_fired, "the rest edge fired the (gate-armed) echo seam");

+            assert!(gate.exists(), "gate armed — ling's own pulse fires the real commune");

+            assert_eq!(read_rest(&perch_path).unwrap().state, RestState::Dormant);

+            // wake: no gate re-arm; the freshness-pull marker drops instead.

+            std::fs::remove_file(&gate).unwrap();

+            let report = daemon_rest_event("ling", RestEvent::Wake, None)

+            assert_eq!(report.to, RestState::Active);

+            assert!(!gate.exists(), "wake arms no echo");

+                perch::identity_dir().join(PULL_MARKER_FILE).exists(),

+                "wake dropped the freshness-pull marker"

// consumed exactly once — two pump ticks can never both claim one wake.


Lifecycle pulse-gating test:


// [unit->REQ-INST-4] the daemon-hosted rest edge fires the REAL transition

    // [unit->REQ-INST-3] pulse recency follows the resting model (the D8 gap
    // closed): an active/recordless tick stamps last_active_ms, a DORMANT
    // tick does NOT (a resting seat must never win a most-recently-active
    // resolution) — and a dormant seat past the opt-in auto-suspend
    // threshold suspends from the tick itself.
    #[test]
    fn pulse_recency_and_auto_suspend_follow_resting_state() {
        use spt_store::info::read_info;

        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let manifest = live_manifest(drops.path());
            // Node knob ON with a tiny threshold (the endpoint inherits it).
            let cfg = DaemonConfig { auto_suspend_after_ms: Some(1), ..DaemonConfig::default() };
            let host = BrainLifecycle::with_config(&manifest, "doyle", cfg).expect("live host");
            let perch_path = seed_supervised_perch("doyle", std::process::id());

            // Recordless (pre-D9-2 parity): the tick stamps recency.
            host.pulse_tick(Some("sid-1")).expect("tick");
            let stamped = read_info(&perch_path).unwrap().last_active_ms.expect("stamped");

            // Dormant: the tick must NOT refresh recency — and with the node
            // knob on + a stale dormancy anchor, it auto-suspends instead.
            crate::resting::write_rest(&perch_path, crate::resting::RestState::Dormant, 1_000)
                .unwrap();
            host.pulse_tick(Some("sid-1")).expect("tick");
            let rec = read_info(&perch_path).unwrap();
            assert_eq!(
                rec.last_active_ms,
                Some(stamped),
                "a resting seat's pulse must not win recency"
            );
            assert_eq!(
                rec.rest_state.as_deref(),
                Some("suspended"),
                "stale dormant seat auto-suspended from the tick (knob chain: node leg)"
            );

            // Suspended: the tick neither stamps nor transitions.
            host.pulse_tick(Some("sid-1")).expect("tick");
            let rec = read_info(&perch_path).unwrap();
            assert_eq!(rec.last_active_ms, Some(stamped));
            assert_eq!(rec.rest_state.as_deref(), Some("suspended"));
        });
    }

    // [unit->REQ-INST-4] the daemon-hosted rest edge fires the REAL transition


    // [unit->REQ-INST-3] pulse recency follows the resting model (the D8 gap
    // closed): an active/recordless tick stamps last_active_ms, a DORMANT
    // tick does NOT (a resting seat must never win a most-recently-active
    // resolution) — and a dormant seat past the opt-in auto-suspend
    // threshold suspends from the tick itself.
    #[test]
    fn pulse_recency_and_auto_suspend_follow_resting_state() {
        use spt_store::info::read_info;

        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let manifest = live_manifest(drops.path());
            // Node knob ON with a tiny threshold (the endpoint inherits it).
            let cfg = DaemonConfig { auto_suspend_after_ms: Some(1), ..DaemonConfig::default() };
            let host = BrainLifecycle::with_config(&manifest, "doyle", cfg).expect("live host");
            let perch_path = seed_supervised_perch("doyle", std::process::id());

            // Recordless (pre-D9-2 parity): the tick stamps recency.
            host.pulse_tick(Some("sid-1")).expect("tick");
            let stamped = read_info(&perch_path).unwrap().last_active_ms.expect("stamped");

            // Dormant: the tick must NOT refresh recency — and with the node
            // knob on + a stale dormancy anchor, it auto-suspends instead.
            crate::resting::write_rest(&perch_path, crate::resting::RestState::Dormant, 1_000)
                .unwrap();
            host.pulse_tick(Some("sid-1")).expect("tick");
            let rec = read_info(&perch_path).unwrap();
            assert_eq!(
                rec.last_active_ms,
                Some(stamped),
                "a resting seat's pulse must not win recency"
            );
            assert_eq!(
                rec.rest_state.as_deref(),
                Some("suspended"),
                "stale dormant seat auto-suspended from the tick (knob chain: node leg)"
            );

            // Suspended: the tick neither stamps nor transitions.
            host.pulse_tick(Some("sid-1")).expect("tick");
            let rec = read_info(&perch_path).unwrap();
            assert_eq!(rec.last_active_ms, Some(stamped));
            assert_eq!(rec.rest_state.as_deref(), Some("suspended"));
        });
    }

    // [unit->REQ-INST-4] the daemon-hosted rest edge fires the REAL transition

+    // [unit->REQ-INST-3] pulse recency follows the resting model (the D8 gap

+    // closed): an active/recordless tick stamps last_active_ms, a DORMANT

+    // tick does NOT (a resting seat must never win a most-recently-active

+    // resolution) — and a dormant seat past the opt-in auto-suspend

+    // threshold suspends from the tick itself.

+    fn pulse_recency_and_auto_suspend_follow_resting_state() {

+        use spt_store::info::read_info;

+            let drops = tempfile::tempdir().unwrap();

+            let manifest = live_manifest(drops.path());

+            // Node knob ON with a tiny threshold (the endpoint inherits it).

+            let cfg = DaemonConfig { auto_suspend_after_ms: Some(1), ..DaemonConfig::default() };

+            let host = BrainLifecycle::with_config(&manifest, "doyle", cfg).expect("live host");

+            let perch_path = seed_supervised_perch("doyle", std::process::id());

+            // Recordless (pre-D9-2 parity): the tick stamps recency.

+            host.pulse_tick(Some("sid-1")).expect("tick");

+            let stamped = read_info(&perch_path).unwrap().last_active_ms.expect("stamped");

+            // Dormant: the tick must NOT refresh recency — and with the node

+            // knob on + a stale dormancy anchor, it auto-suspends instead.

+            crate::resting::write_rest(&perch_path, crate::resting::RestState::Dormant, 1_000)

+            let rec = read_info(&perch_path).unwrap();

+                "a resting seat's pulse must not win recency"

+                rec.rest_state.as_deref(),

+                "stale dormant seat auto-suspended from the tick (knob chain: node leg)"

+            // Suspended: the tick neither stamps nor transitions.

+            assert_eq!(rec.last_active_ms, Some(stamped));

+            assert_eq!(rec.rest_state.as_deref(), Some("suspended"));

// echo commune (run_echo_commune through the manifest summarizer) exactly

// once per active→resting edge — the final context delta lands in the


id = "REQ-INST-6"
title = "Deferred messages not delivered to dormant/suspended instances"
required_stages = []

id = "REQ-INST-6"
title = "Deferred messages not delivered to dormant/suspended instances"
required_stages = []  # rule 5: M5. The precondition shipped M4-D9-2 (the explicit resting-state machine + durable info.json rest_state record — the gate's input is now a readable fact); the gate itself (deferred-row hold while resting, release on the wake edge) rides the M5 delivery work. Seam documented in spt-daemon::resting module docs


The file C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml has been updated successfully. (file state is current in your context — no need to Read it back)

id = "REQ-INST-6"
title = "Deferred messages not delivered to dormant/suspended instances"
required_stages = []  # rule 5: M5. The precondition shipped M4-D9-2 (the explicit resting-state machine + durable info.json rest_state record — the gate's input is now a readable fact); the gate itself (deferred-row hold while resting, release on the wake edge) rides the M5 delivery work. Seam documented in spt-daemon::resting module docs

+required_stages = []  # rule 5: M5. The precondition shipped M4-D9-2 (the explicit resting-state machine + durable info.json rest_state record — the gate's input is now a readable fact); the gate itself (deferred-row hold while resting, release on the wake edge) rides the M5 delivery work. Seam documented in spt-daemon::resting module docs

title = "Subnet registry + bare-id resolution policy"


required_stages = []  # rule 5: activate when the daemon multi-agent pulse fan-out is wired. Today run_pulse_loop is def+test only and each agent drives its own pulse from its own process, so blocking is naturally isolated; the invariant binds once the daemon hosts N per-agent loops

required_stages = []  # rule 5: activate when the daemon multi-agent pulse fan-out is wired. Today run_pulse_loop is def+test only and each agent drives its own pulse from its own process, so blocking is naturally isolated; the invariant binds once the daemon hosts N per-agent loops. M4-D9-2-3 note: the daemon's resting-edge feed points (attach detach, registry attention-shift, spt suspend) deliberately ARM the per-endpoint echo gate (resting::arm_transition_echo) instead of hosting the bounded call — N flips in one feed are N gate files consumed by N per-agent pulse loops, so the invariant holds by construction and the binding moment stays the per-agent-runtime fan-out (same seam as the in-pump Psyche reconcile turn)


title = "Per-agent pulse/psyche/echo-commune scheduling must not serialize across agents: each agent's bounded LLM call (echo-commune summarizer, Psyche turn) runs off the shared scheduler so one slow/hung call cannot stall another agent's tick (7.4)"

-required_stages = []  # rule 5: activate when the daemon multi-agent pulse fan-out is wired. Today run_pulse_loop is def+test only and each agent drives its own pulse from its own process, so blocking is naturally isolated; the invariant binds once the daemon hosts N per-agent loops

+required_stages = []  # rule 5: activate when the daemon multi-agent pulse fan-out is wired. Today run_pulse_loop is def+test only and each agent drives its own pulse from its own process, so blocking is naturally isolated; the invariant binds once the daemon hosts N per-agent loops. M4-D9-2-3 note: the daemon's resting-edge feed points (attach detach, registry attention-shift, spt suspend) deliberately ARM the per-endpoint echo gate (resting::arm_transition_echo) instead of hosting the bounded call — N flips in one feed are N gate files consumed by N per-agent pulse loops, so the invariant holds by construction and the binding moment stays the per-agent-runtime fan-out (same seam as the in-pump Psyche reconcile turn)

# --- Added 2026-06-03 at M4-D2 start (ADR-0005 Stage-A red-team #10/#11/#12); rule 3: register before satisfying, rule 5: activate per sub-task commit ---


Build + test + clippy + traceability:


PowerShell

cargo test -p spt-daemon --lib 2>&1 | Select-String -Pattern "test result:|error\[|failures:" | Select-Object -First 8

spt-daemon lib tests


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


cargo clippy --workspace --all-targets -- -D warnings 2>&1 | Select-Object -Last 3; cargo test --workspace *> $null; "TESTS_EXIT=$LASTEXITCODE"; traceable-reqs check *> $null; "TRACE_EXIT=$LASTEXITCODE"

Clippy + full tests + traceability


    Checking spt-daemon v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Checking spt v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.44s
TESTS_EXIT=0
TRACE_EXIT=0


All gates green locally. Check D9-2-2 CI FINAL before pushing:


gh run list --branch dev-freeform --limit 2 --json status,conclusion,headSha | ConvertFrom-Json | Format-Table

Check D9-2-2 CI status


conclusion headSha                                  status
---------- -------                                  ------
success    8a7afd6792b75562b7699fc89b36c79d9db2b656 completed
success    031578398664aa6559552f2dfd0d399928d5684d completed


Updated task #2 status


git add -A; git diff --cached --stat | Select-Object -Last 9

Stage D9-2-3


warning: in the working copy of 'crates/spt-daemon/src/attach.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/dispatch.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/lib.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/lifecycle.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/registryhost.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/resting.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/src/cli.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'traceable-reqs.toml', LF will be replaced by CRLF the next time Git touches it
 crates/spt-daemon/src/attach.rs       |  18 ++++++
 crates/spt-daemon/src/dispatch.rs     |  28 +++++++-
 crates/spt-daemon/src/lib.rs          |   5 +-
 crates/spt-daemon/src/lifecycle.rs    |  87 +++++++++++++++++++------
 crates/spt-daemon/src/registryhost.rs |  94 +++++++++++++++++++++++++--
 crates/spt-daemon/src/resting.rs      | 117 ++++++++++++++++++++++++++++++++++
 crates/spt/src/cli.rs                 |  57 +++++++++++++++++
 traceable-reqs.toml                   |   4 +-
 8 files changed, 383 insertions(+), 27 deletions(-)


git commit -m @'
feat(daemon,spt): M4-D9-2-3 resting-edge production feeds + spt suspend/wake

The state machine's events now arrive from real observation points:

- attach arm: a served viewport IS a driver — Request feeds Wake
  (re-activate in place, idempotent on an active seat); viewport end
  (detach/exit/error) feeds Detach (rest warm). Best-effort like
  mark_driven_by.
- registry arm: RegistryHost::apply_feed_flips reports observed
  transitions-to-Active on peer nodes — the attention-shift wire signal
  (driving ling@laptop makes ling@desktop dormant). Never Inserted (a
  restarted daemon re-learning rows must not knock local seats dormant),
  never an Active re-advertisement, never past the lease. The dispatch
  arm feeds AttentionShift for locally hosted ids only.
- pulse: recency follows the resting model (the D8 gap closed) — only an
  active/recordless tick stamps last_active_ms, so a resting seat never
  wins a most-recently-active resolution; a dormant tick checks the
  opt-in auto-suspend threshold instead; a suspended tick just ingests.
- spt suspend / spt wake: the manual edges, local arm (the id@node
  suspend-from-anywhere arm composes with M5 remote machinery).

Daemon-side echo posture (KH 7.4 by construction): feed points hold no
manifest runtime, so the rest-edge echo is GATE-ARMED
(resting::arm_transition_echo) — the endpoint's own pulse loop runs the
bounded summarizer call; N flips in one feed are N gate files, never N
LLM calls serialized in a dispatch worker. The hazard requirement stays
unbound until the daemon hosts per-agent runtimes (the 7.4 fan-out seam).

REQ-INST-6 (deferred-message instance-state gate) stays inactive — the
machine is its precondition, the gate is M5 (seam noted in the registry).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@ && git push origin dev-freeform

Commit and push D9-2-3


[dev-freeform 56a98db] feat(daemon,spt): M4-D9-2-3 resting-edge production feeds + spt suspend/wake
 8 files changed, 383 insertions(+), 27 deletions(-)
To https://github.com/SaberMage/spt-core.git
   8a7afd6..56a98db  dev-freeform -> dev-freeform


Pushed. Poll CI:


Start-Sleep -Seconds 45; gh run list --branch dev-freeform --limit 2 --json status,conclusion,headSha | ConvertFrom-Json | Format-Table

Wait then check CI for 56a98db


<tool_use_error>Blocked: Start-Sleep 45 followed by: gh run list --branch dev-freeform --limit 2 --json status,conclusion,headSha | ConvertFrom-Json | Format-Table. To wait for a condition, use Monitor with an until-loop (e.g. `until <check>; do sleep 2; done` — Monitor runs bash). To wait for a command you started, use run_in_background: true. Do not chain shorter sleeps to work around this block.</tool_use_error>

Error: Blocked: Start-Sleep 45 followed by: gh run list --branch dev-freeform --limit 2 --json status,conclusion,headSha | ConvertFrom-Json | Format-Table. To wait for a condition, use Monitor with an until-loop (e.g. `until <check>; do sleep 2; done` — Monitor runs bash). To wait for a command you started, use run_in_background: true. Do not chain shorter sleeps to work around this block.


ToolSearch

Question: select:Monitor


Question: select:Monitor


Bash

until [ "$(gh run list --branch dev-freeform --limit 1 --json status,conclusion,headSha --jq '.[0] | select(.headSha=="56a98db8") // .[0] | .status')" = "completed" ]; do sleep 20; done; gh run list --branch dev-freeform --limit 1 --json status,conclusion,headSha --jq '.[0] | "\(.headSha[0:7]) \(.status) \(.conclusion)"'

Wait for D9-2-3 CI run to complete


Command running in background with ID: b6jd121hg. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\b6jd121hg.output. You will be notified when it completes. To check interim output, use Read on that file path.


While CI runs — prep plan-status update for D9-2: