    // [unit->REQ-INST-4] the transition echo fires EXACTLY ONCE per
    // active→resting edge: the first detach runs the echo before the flip
    // persists; replaying the same event is a no-edge and can never re-fire
    // it; the dormant→suspended edge (already resting) fires no echo; and
    // the wake edge runs the wake effects instead.
    #[test]
    fn apply_event_fires_echo_once_per_rest_edge_and_wake_on_wake() {
        let d = tempfile::tempdir().unwrap();
        write_info(d.path(), &InfoJson::new("ling", "t", 4242, "sid", "live_agent")).unwrap();

        let mut echoes = 0;
        let mut wakes = 0;

        // Recordless live perch starts Active: detach = the rest edge.
        let report = apply_event(
            d.path(),
            RestEvent::Detach,
            None,
            1_000,
            || {
                echoes += 1;
                Ok(())
            },
            || wakes += 1,
        )
        .expect("apply ok")
        .expect("a real edge");
        assert_eq!((report.from, report.to), (RestState::Active, RestState::Dormant));
        assert!(report.echo_fired, "active→dormant fires the transition echo");
        assert_eq!((echoes, wakes), (1, 0));
        assert_eq!(read_rest(d.path()).unwrap().state, RestState::Dormant, "flip persisted");

        // Replay: no edge, no second echo — idempotent by the pure table.
        let replay = apply_event(d.path(), RestEvent::Detach, None, 1_100, || {
            echoes += 1;
            Ok(())
        }, || wakes += 1)
        .expect("apply ok");
        assert_eq!(replay, None, "replayed event is a no-edge");
        assert_eq!(echoes, 1, "echo fired exactly once per edge");

        // dormant → suspended: already resting — NO echo (the echo captures
        // the *outgoing active* instance's delta; it already ran).
        let report = apply_event(d.path(), RestEvent::Suspend, None, 1_200, || {
            echoes += 1;
            Ok(())
        }, || wakes += 1)
        .expect("apply ok")
        .expect("edge");
        assert_eq!(report.to, RestState::Suspended);
        assert!(!report.echo_fired, "resting→resting fires no echo");
        assert_eq!(echoes, 1);

        // wake: the wake effects fire, no echo.
        let report = apply_event(d.path(), RestEvent::Wake, None, 1_300, || {
            echoes += 1;
            Ok(())
        }, || wakes += 1)
        .expect("apply ok")
        .expect("edge");
        assert_eq!((report.from, report.to), (RestState::Suspended, RestState::Active));
        assert_eq!((echoes, wakes), (1, 1), "wake runs wake effects, not the echo");
        assert_eq!(read_rest(d.path()).unwrap().state, RestState::Active);
    }

    // [unit->REQ-INST-4] a failing echo is LOUD but does not wedge the
    // instance active: the state persists (the driver is gone — that is a
    // fact), the error surfaces after (the signoff_with posture).
    #[test]
    fn apply_event_echo_failure_is_loud_but_state_persists() {
        let d = tempfile::tempdir().unwrap();
        write_info(d.path(), &InfoJson::new("ling", "t", 4242, "sid", "live_agent")).unwrap();

        let err = apply_event(
            d.path(),
            RestEvent::Detach,
            None,
            1_000,
            || Err("summarizer exploded".to_string()),
            || {},
        )
        .expect_err("echo failure surfaces");
        assert!(err.contains("state persisted"), "loud and honest: {err}");
        assert_eq!(
            read_rest(d.path()).unwrap().state,
            RestState::Dormant,
            "the rest edge is a fact about the driver, not the echo"
        );
    }

    // [unit->REQ-INST-3] apply_event resolves the knob chain end-to-end: a
    // node knob auto-suspends a stale dormant perch from the tick, and the
    // endpoint 0-override holds it warm against the same node knob.
    #[test]
    fn apply_event_auto_suspends_under_the_resolved_chain() {
        let d = tempfile::tempdir().unwrap();
        write_info(d.path(), &InfoJson::new("ling", "t", 4242, "sid", "live_agent")).unwrap();
        write_rest(d.path(), RestState::Dormant, 1_000).unwrap();

        // Node enables 500ms; dormant since 1000; tick at 2000 ⇒ suspended.
        let report = apply_event(
            d.path(),
            RestEvent::AutoSuspendTick { now_ms: 2_000 },
            Some(500),
            2_000,
            || Ok(()),
            || {},
        )
        .expect("apply ok")
        .expect("edge");
        assert_eq!(report.to, RestState::Suspended);
        assert!(!report.echo_fired, "dormant→suspended: the echo already ran at the rest edge");

        // Endpoint 0-override: same node knob, but the endpoint opted out.
        let d2 = tempfile::tempdir().unwrap();
        write_info(d2.path(), &InfoJson::new("oak", "t", 4242, "sid", "live_agent")).unwrap();
        spt_store::info::read_info(d2.path()).unwrap(); // record exists
        let mut rec = spt_store::info::read_info(d2.path()).unwrap();
        rec.auto_suspend_after_ms = Some(0);
        spt_store::info::write_info(d2.path(), &rec).unwrap();
        write_rest(d2.path(), RestState::Dormant, 1_000).unwrap();
        let out = apply_event(
            d2.path(),
            RestEvent::AutoSuspendTick { now_ms: u64::MAX },
            Some(500),
            2_000,
            || Ok(()),
            || {},
        )
        .expect("apply ok");
        assert_eq!(out, None, "endpoint 0 = explicit OFF beats the node knob");
    }

    // [unit->REQ-INST-3] the freshness-pull marker: dropped idempotently,
    // consumed exactly once — two pump ticks can never both claim one wake.
    #[test]
    fn freshness_pull_marker_is_taken_exactly_once() {
        let d = tempfile::tempdir().unwrap();
        let marker = d.path().join("identity").join(PULL_MARKER_FILE);

        assert!(!take_freshness_pull(&marker), "nothing to take yet");
        request_freshness_pull(&marker).unwrap();
        request_freshness_pull(&marker).unwrap(); // re-request is idempotent
        assert!(take_freshness_pull(&marker), "first take claims the wake");
        assert!(!take_freshness_pull(&marker), "second take gets nothing");
    }

    // [unit->REQ-INST-3] durability round-trip: write_rest stamps the

+    // [unit->REQ-INST-4] the transition echo fires EXACTLY ONCE per

+    // active→resting edge: the first detach runs the echo before the flip

+    // persists; replaying the same event is a no-edge and can never re-fire

+    // it; the dormant→suspended edge (already resting) fires no echo; and

+    // the wake edge runs the wake effects instead.

+    fn apply_event_fires_echo_once_per_rest_edge_and_wake_on_wake() {

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

+        write_info(d.path(), &InfoJson::new("ling", "t", 4242, "sid", "live_agent")).unwrap();

+        // Recordless live perch starts Active: detach = the rest edge.

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

+        assert!(report.echo_fired, "active→dormant fires the transition echo");

+        assert_eq!((echoes, wakes), (1, 0));

+        assert_eq!(read_rest(d.path()).unwrap().state, RestState::Dormant, "flip persisted");

+        // Replay: no edge, no second echo — idempotent by the pure table.

+        let replay = apply_event(d.path(), RestEvent::Detach, None, 1_100, || {

+        assert_eq!(replay, None, "replayed event is a no-edge");

+        assert_eq!(echoes, 1, "echo fired exactly once per edge");

+        // dormant → suspended: already resting — NO echo (the echo captures

+        // the *outgoing active* instance's delta; it already ran).

+        let report = apply_event(d.path(), RestEvent::Suspend, None, 1_200, || {

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

+        assert!(!report.echo_fired, "resting→resting fires no echo");

+        // wake: the wake effects fire, no echo.

+        let report = apply_event(d.path(), RestEvent::Wake, None, 1_300, || {

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

+        assert_eq!((echoes, wakes), (1, 1), "wake runs wake effects, not the echo");

+        assert_eq!(read_rest(d.path()).unwrap().state, RestState::Active);

+    // [unit->REQ-INST-4] a failing echo is LOUD but does not wedge the

+    // instance active: the state persists (the driver is gone — that is a

+    // fact), the error surfaces after (the signoff_with posture).

+    fn apply_event_echo_failure_is_loud_but_state_persists() {

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

+            || Err("summarizer exploded".to_string()),

+        .expect_err("echo failure surfaces");

+        assert!(err.contains("state persisted"), "loud and honest: {err}");

+            read_rest(d.path()).unwrap().state,

+            "the rest edge is a fact about the driver, not the echo"

+    // [unit->REQ-INST-3] apply_event resolves the knob chain end-to-end: a

+    // node knob auto-suspends a stale dormant perch from the tick, and the

+    // endpoint 0-override holds it warm against the same node knob.

+    fn apply_event_auto_suspends_under_the_resolved_chain() {

+        write_rest(d.path(), RestState::Dormant, 1_000).unwrap();

+        // Node enables 500ms; dormant since 1000; tick at 2000 ⇒ suspended.

+            RestEvent::AutoSuspendTick { now_ms: 2_000 },

+        assert!(!report.echo_fired, "dormant→suspended: the echo already ran at the rest edge");

+        // Endpoint 0-override: same node knob, but the endpoint opted out.

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

+        write_info(d2.path(), &InfoJson::new("oak", "t", 4242, "sid", "live_agent")).unwrap();

+        spt_store::info::read_info(d2.path()).unwrap(); // record exists

+        let mut rec = spt_store::info::read_info(d2.path()).unwrap();

+        rec.auto_suspend_after_ms = Some(0);

+        spt_store::info::write_info(d2.path(), &rec).unwrap();

+        write_rest(d2.path(), RestState::Dormant, 1_000).unwrap();

+            RestEvent::AutoSuspendTick { now_ms: u64::MAX },

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

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

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

+    fn freshness_pull_marker_is_taken_exactly_once() {

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

+        let marker = d.path().join("identity").join(PULL_MARKER_FILE);

+        assert!(!take_freshness_pull(&marker), "nothing to take yet");

+        request_freshness_pull(&marker).unwrap(); // re-request is idempotent

+        assert!(take_freshness_pull(&marker), "first take claims the wake");

+        assert!(!take_freshness_pull(&marker), "second take gets nothing");

// dormancy anchor exactly on the dormant state (cleared otherwise),

// read_rest reads it back, a recordless perch reads None (legacy


Edit

        write_info(d2.path(), &InfoJson::new("oak", "t", 4242, "sid", "live_agent")).unwrap();
        spt_store::info::read_info(d2.path()).unwrap(); // record exists
        let mut rec = spt_store::info::read_info(d2.path()).unwrap();

        write_info(d2.path(), &InfoJson::new("oak", "t", 4242, "sid", "live_agent")).unwrap();
        let mut rec = spt_store::info::read_info(d2.path()).unwrap();


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)

        write_info(d2.path(), &InfoJson::new("oak", "t", 4242, "sid", "live_agent")).unwrap();
        spt_store::info::read_info(d2.path()).unwrap(); // record exists
        let mut rec = spt_store::info::read_info(d2.path()).unwrap();

        write_info(d2.path(), &InfoJson::new("oak", "t", 4242, "sid", "live_agent")).unwrap();
        let mut rec = spt_store::info::read_info(d2.path()).unwrap();

// Endpoint 0-override: same node knob, but the endpoint opted out.

-        spt_store::info::read_info(d2.path()).unwrap(); // record exists


Now lifecycle glue. Verify NotifSurfacePolicy::load + NotifStore::open:


Grep

impl NotifSurfacePolicy|pub fn load\(|pub fn open\(


69:impl NotifSurfacePolicy {
70-    /// Load from the node's canonical stores (`subnet.json`, `visibility.json`).
71:    pub fn load() -> NotifSurfacePolicy {
72-        let subnets = spt_store::subnet::SubnetStore::load()
73-            .subnets
74-            .iter()
75-            .map(|s| (s.name.clone(), s.hide_new_endpoints))
76-            .collect();
77-        NotifSurfacePolicy { subnets, vis: VisibilityStore::load() }
78-    }
79-
80-    \ Whether `endpoint` is a legitimate surface for notifs in `subnet`:
81-    /// this node is a member AND the endpoint is not hidden there


Edit

    /// Run the echo-commune into the commune dir (T3). No-op (Ok) without a dir.
    fn fire_echo(&self, session_id: Option<&str>) -> Result<(), String> {

    /// Apply a resting-state event for this endpoint (D9-2, REQ-INST-3) — the
    /// production transition host wiring [`crate::resting::apply_event`]'s two
    /// effect seams to the real machinery:
    ///
    /// - **active → (dormant|suspended)**: the transition echo commune
    ///   ([`fire_echo`](Self::fire_echo) → [`run_echo_commune`]) capturing the
    ///   outgoing instance's final context delta, BEFORE the flip persists
    ///   (KH 3.3) — it syncs to whichever instance activates next.
    /// - **wake (→ active)**: re-activation in place catches up immediately —
    ///   [`crate::notif::resurface_at_boundary`] at the Wake boundary (the D8
    ///   deferral, real now that the edge exists) + the freshness-pull marker
    ///   the peer pump consumes (the D9-1 seam: pull the latest mind NOW, not
    ///   at the next anti-entropy cadence).
    ///
    /// Registry advertisement follows on the pump's next `advertise_local`
    /// scan — the persisted record is what it reads, epoch-bumped per row.
    // [impl->REQ-INST-3]
    // [impl->REQ-INST-4]
    pub fn rest_event(
        &self,
        session_id: Option<&str>,
        event: crate::resting::RestEvent,
    ) -> Result<Option<crate::resting::EdgeReport>, String> {
        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);
        crate::resting::apply_event(
            &perch_path,
            event,
            self.cfg.auto_suspend_after_ms,
            now_ms(),
            || self.fire_echo(session_id),
            || self.fire_wake_effects(),
        )
    }

    /// 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);
    }

    /// Run the echo-commune into the commune dir (T3). No-op (Ok) without a dir.
    fn fire_echo(&self, session_id: Option<&str>) -> Result<(), String> {


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)

    /// Apply a resting-state event for this endpoint (D9-2, REQ-INST-3) — the
    /// production transition host wiring [`crate::resting::apply_event`]'s two
    /// effect seams to the real machinery:
    ///
    /// - **active → (dormant|suspended)**: the transition echo commune
    ///   ([`fire_echo`](Self::fire_echo) → [`run_echo_commune`]) capturing the
    ///   outgoing instance's final context delta, BEFORE the flip persists
    ///   (KH 3.3) — it syncs to whichever instance activates next.
    /// - **wake (→ active)**: re-activation in place catches up immediately —
    ///   [`crate::notif::resurface_at_boundary`] at the Wake boundary (the D8
    ///   deferral, real now that the edge exists) + the freshness-pull marker
    ///   the peer pump consumes (the D9-1 seam: pull the latest mind NOW, not
    ///   at the next anti-entropy cadence).
    ///
    /// Registry advertisement follows on the pump's next `advertise_local`
    /// scan — the persisted record is what it reads, epoch-bumped per row.
    // [impl->REQ-INST-3]
    // [impl->REQ-INST-4]
    pub fn rest_event(
        &self,
        session_id: Option<&str>,
        event: crate::resting::RestEvent,
    ) -> Result<Option<crate::resting::EdgeReport>, String> {
        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);
        crate::resting::apply_event(
            &perch_path,
            event,
            self.cfg.auto_suspend_after_ms,
            now_ms(),
            || self.fire_echo(session_id),
            || self.fire_wake_effects(),
        )
    }

    /// 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);
    }

    /// Run the echo-commune into the commune dir (T3). No-op (Ok) without a dir.
    fn fire_echo(&self, session_id: Option<&str>) -> Result<(), String> {

+    /// Apply a resting-state event for this endpoint (D9-2, REQ-INST-3) — the

+    /// production transition host wiring [`crate::resting::apply_event`]'s two

+    /// effect seams to the real machinery:

+    /// - **active → (dormant|suspended)**: the transition echo commune

+    ///   ([`fire_echo`](Self::fire_echo) → [`run_echo_commune`]) capturing the

+    ///   outgoing instance's final context delta, BEFORE the flip persists

+    ///   (KH 3.3) — it syncs to whichever instance activates next.

+    /// - **wake (→ active)**: re-activation in place catches up immediately —

+    ///   [`crate::notif::resurface_at_boundary`] at the Wake boundary (the D8

+    ///   deferral, real now that the edge exists) + the freshness-pull marker

+    ///   the peer pump consumes (the D9-1 seam: pull the latest mind NOW, not

+    ///   at the next anti-entropy cadence).

+    /// Registry advertisement follows on the pump's next `advertise_local`

+    /// scan — the persisted record is what it reads, epoch-bumped per row.

+        event: crate::resting::RestEvent,

+    ) -> Result<Option<crate::resting::EdgeReport>, String> {

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

+            self.cfg.auto_suspend_after_ms,

+            || self.fire_echo(session_id),

+            || self.fire_wake_effects(),

+    /// 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.

+        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);

let Some(dir) = &self.commune_dir else {


Peerloop: marker field + consume:


    /// The staged-release cache dir ([`ReleaseCache::open`]).
    pub releases: PathBuf,
    /// `release-keys.json` ([`VerifyPolicy::load_from`]).
    pub release_keys: PathBuf,
}

    /// The staged-release cache dir ([`ReleaseCache::open`]).
    pub releases: PathBuf,
    /// `release-keys.json` ([`VerifyPolicy::load_from`]).
    pub release_keys: PathBuf,
    /// The wake-edge freshness-pull marker (D9-2, REQ-INST-3): a
    /// dormant→active transition drops it; the pump consumes it and forces a
    /// sync round NOW instead of waiting out `cadence.sync`
    /// ([`crate::resting::PULL_MARKER_FILE`]).
    pub pull_marker: PathBuf,
}


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

    /// The staged-release cache dir ([`ReleaseCache::open`]).
    pub releases: PathBuf,
    /// `release-keys.json` ([`VerifyPolicy::load_from`]).
    pub release_keys: PathBuf,
}

    /// The staged-release cache dir ([`ReleaseCache::open`]).
    pub releases: PathBuf,
    /// `release-keys.json` ([`VerifyPolicy::load_from`]).
    pub release_keys: PathBuf,
    /// The wake-edge freshness-pull marker (D9-2, REQ-INST-3): a
    /// dormant→active transition drops it; the pump consumes it and forces a
    /// sync round NOW instead of waiting out `cadence.sync`
    /// ([`crate::resting::PULL_MARKER_FILE`]).
    pub pull_marker: PathBuf,
}

+    /// The wake-edge freshness-pull marker (D9-2, REQ-INST-3): a

+    /// dormant→active transition drops it; the pump consumes it and forces a

+    /// sync round NOW instead of waiting out `cadence.sync`

+    /// ([`crate::resting::PULL_MARKER_FILE`]).


            ops: identity.join("pump-ops.json"),
            releases: home.join("releases"),
            release_keys: identity.join("release-keys.json"),
        }
    }

            ops: identity.join("pump-ops.json"),
            releases: home.join("releases"),
            release_keys: identity.join("release-keys.json"),
            pull_marker: identity.join(crate::resting::PULL_MARKER_FILE),
        }
    }


            ops: identity.join("pump-ops.json"),
            releases: home.join("releases"),
            release_keys: identity.join("release-keys.json"),
        }
    }

            ops: identity.join("pump-ops.json"),
            releases: home.join("releases"),
            release_keys: identity.join("release-keys.json"),
            pull_marker: identity.join(crate::resting::PULL_MARKER_FILE),
        }
    }

+            pull_marker: identity.join(crate::resting::PULL_MARKER_FILE),


    while !stop.load(Ordering::Relaxed) {
        let now = Instant::now();
        let due_reg = now.duration_since(last_reg) >= cadence.registry;
        let due_notif = now.duration_since(last_notif) >= cadence.notif;
        let due_sync = now.duration_since(last_sync) >= cadence.sync;
        let due_upd = now.duration_since(last_upd) >= cadence.update;

    while !stop.load(Ordering::Relaxed) {
        let now = Instant::now();
        let due_reg = now.duration_since(last_reg) >= cadence.registry;
        let due_notif = now.duration_since(last_notif) >= cadence.notif;
        // The wake edge's freshness pull (D9-2, REQ-INST-3): a dormant→active
        // transition dropped the marker — consume it and pull NOW, the
        // catch-up-on-activation latency is the point. The take is the remove,
        // so one wake claims exactly one forced round.
        // [impl->REQ-INST-3]
        let wake_pull = crate::resting::take_freshness_pull(&paths.pull_marker);
        let due_sync = wake_pull || now.duration_since(last_sync) >= cadence.sync;
        let due_upd = now.duration_since(last_upd) >= cadence.update;


    while !stop.load(Ordering::Relaxed) {
        let now = Instant::now();
        let due_reg = now.duration_since(last_reg) >= cadence.registry;
        let due_notif = now.duration_since(last_notif) >= cadence.notif;
        let due_sync = now.duration_since(last_sync) >= cadence.sync;
        let due_upd = now.duration_since(last_upd) >= cadence.update;

    while !stop.load(Ordering::Relaxed) {
        let now = Instant::now();
        let due_reg = now.duration_since(last_reg) >= cadence.registry;
        let due_notif = now.duration_since(last_notif) >= cadence.notif;
        // The wake edge's freshness pull (D9-2, REQ-INST-3): a dormant→active
        // transition dropped the marker — consume it and pull NOW, the
        // catch-up-on-activation latency is the point. The take is the remove,
        // so one wake claims exactly one forced round.
        // [impl->REQ-INST-3]
        let wake_pull = crate::resting::take_freshness_pull(&paths.pull_marker);
        let due_sync = wake_pull || now.duration_since(last_sync) >= cadence.sync;
        let due_upd = now.duration_since(last_upd) >= cadence.update;

-        let due_sync = now.duration_since(last_sync) >= cadence.sync;

+        // The wake edge's freshness pull (D9-2, REQ-INST-3): a dormant→active

+        // transition dropped the marker — consume it and pull NOW, the

+        // catch-up-on-activation latency is the point. The take is the remove,

+        // so one wake claims exactly one forced round.

+        let wake_pull = crate::resting::take_freshness_pull(&paths.pull_marker);

+        let due_sync = wake_pull || now.duration_since(last_sync) >= cadence.sync;

if !(due_reg || due_notif || due_sync || due_upd) {


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.


Update tests/peerloop.rs PumpPaths literals:


        ops: a_root.join("pump-ops.json"),
        releases: a_root.join("releases"),
        release_keys: a_root.join("release-keys.json"),
    };

        ops: a_root.join("pump-ops.json"),
        releases: a_root.join("releases"),
        release_keys: a_root.join("release-keys.json"),
        pull_marker: a_root.join("sync-pull-now"),
    };


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

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


            ops: a_paths.ops.clone(),
            releases: a_paths.releases.clone(),
            release_keys: a_paths.release_keys.clone(),
        };

            ops: a_paths.ops.clone(),
            releases: a_paths.releases.clone(),
            release_keys: a_paths.release_keys.clone(),
            pull_marker: a_paths.pull_marker.clone(),
        };


182	    // ── A (explicit roots): a perch to advertise, a notif row to push, an
183	    //    empty context store that will receive the bootstrap pull. ────────
184	    let a_root = dir.path().join("a");
185	    let a_paths = PumpPaths {
186	        owlery: a_root.join("owlery"),
187	        ctx_root: a_root.join("context"),
188	        notif_db: a_root.join("notifs.db"),
189	        scratch: a_root.join("scratch"),
190	        subnets: a_root.join("subnet.json"),
191	        trust: a_root.join("trust.json"),
192	        vis: a_root.join("visibility.json"),
193	        ops: a_root.join("pump-ops.json"),
194	        releases: a_root.join("releases"),
195	        release_keys: a_root.join("release-keys.json"),
196	    };
197	    // A trusts B's release signer — the update leg's production gate.
198	    std::fs::create_dir_all(&a_root).unwrap();
199	    let keys_json = serde_json::json!({
200	        "keys": { "k": sk.verifying_key().as_bytes().iter()
201	            .map(|b| format!("{b:02x}")).collect::<String>() },

    // ── A (explicit roots): a perch to advertise, a notif row to push, an
    //    empty context store that will receive the bootstrap pull. ────────
    let a_root = dir.path().join("a");
    let a_paths = PumpPaths {
        owlery: a_root.join("owlery"),
        ctx_root: a_root.join("context"),
        notif_db: a_root.join("notifs.db"),
        scratch: a_root.join("scratch"),
        subnets: a_root.join("subnet.json"),
        trust: a_root.join("trust.json"),
        vis: a_root.join("visibility.json"),
        ops: a_root.join("pump-ops.json"),
        releases: a_root.join("releases"),
        release_keys: a_root.join("release-keys.json"),
    };
    // A trusts B's release signer — the update leg's production gate.
    std::fs::create_dir_all(&a_root).unwrap();
    let keys_json = serde_json::json!({
        "keys": { "k": sk.verifying_key().as_bytes().iter()
            .map(|b| format!("{b:02x}")).collect::<String>() },


        ops: a_root.join("pump-ops.json"),
        releases: a_root.join("releases"),
        release_keys: a_root.join("release-keys.json"),
    };

        ops: a_root.join("pump-ops.json"),
        releases: a_root.join("releases"),
        release_keys: a_root.join("release-keys.json"),
        pull_marker: a_root.join("sync-pull-now"),
    };


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

        ops: a_root.join("pump-ops.json"),
        releases: a_root.join("releases"),
        release_keys: a_root.join("release-keys.json"),
    };

        ops: a_root.join("pump-ops.json"),
        releases: a_root.join("releases"),
        release_keys: a_root.join("release-keys.json"),
        pull_marker: a_root.join("sync-pull-now"),
    };

+        pull_marker: a_root.join("sync-pull-now"),

// A trusts B's release signer — the update leg's production gate.


263	    };
264	    {
265	        let name = a_name.clone();
266	        let reg = Arc::clone(&a_registry);
267	        let flag = Arc::clone(&stop);
268	        let paths = PumpPaths {
269	            owlery: a_paths.owlery.clone(),
270	            ctx_root: a_paths.ctx_root.clone(),
271	            notif_db: a_paths.notif_db.clone(),
272	            scratch: a_paths.scratch.clone(),
273	            subnets: a_paths.subnets.clone(),
274	            trust: a_paths.trust.clone(),
275	            vis: a_paths.vis.clone(),
276	            ops: a_paths.ops.clone(),
277	            releases: a_paths.releases.clone(),
278	            release_keys: a_paths.release_keys.clone(),
279	        };
280	        thread::spawn(move || {
281	            let config = PumpConfig {
282	                cadence: PumpCadence {

    };
    {
        let name = a_name.clone();
        let reg = Arc::clone(&a_registry);
        let flag = Arc::clone(&stop);
        let paths = PumpPaths {
            owlery: a_paths.owlery.clone(),
            ctx_root: a_paths.ctx_root.clone(),
            notif_db: a_paths.notif_db.clone(),
            scratch: a_paths.scratch.clone(),
            subnets: a_paths.subnets.clone(),
            trust: a_paths.trust.clone(),
            vis: a_paths.vis.clone(),
            ops: a_paths.ops.clone(),
            releases: a_paths.releases.clone(),
            release_keys: a_paths.release_keys.clone(),
        };
        thread::spawn(move || {
            let config = PumpConfig {
                cadence: PumpCadence {


            ops: a_paths.ops.clone(),
            releases: a_paths.releases.clone(),
            release_keys: a_paths.release_keys.clone(),
        };

            ops: a_paths.ops.clone(),
            releases: a_paths.releases.clone(),
            release_keys: a_paths.release_keys.clone(),
            pull_marker: a_paths.pull_marker.clone(),
        };


            ops: a_paths.ops.clone(),
            releases: a_paths.releases.clone(),
            release_keys: a_paths.release_keys.clone(),
        };

            ops: a_paths.ops.clone(),
            releases: a_paths.releases.clone(),
            release_keys: a_paths.release_keys.clone(),
            pull_marker: a_paths.pull_marker.clone(),
        };

+            pull_marker: a_paths.pull_marker.clone(),


Add lifecycle test. Check NotifStore insert API first:


89:    pub fn open() -> rusqlite::Result<NotifStore> {
90-        Self::open_at(&crate::perch::notifs_file())
91-    }
92-
--
128:    pub fn produce(
129-        &self,
130-        node_hex: &str,
131-        epochs: &mut EpochSource,
--
209:    pub fn mark_seen(&self, notif_id: &str, endpoint: &str) -> rusqlite::Result<bool> {
210-        self.conn.execute_batch("BEGIN IMMEDIATE")?;
211-        let res = (|| -> rusqlite::Result<bool> {
212-            let Some(mut row) = self.read_row(notif_id)? else { return Ok(false) };
--
252:    pub fn undismissed(&self, subnet: &str) -> rusqlite::Result<Vec<NotifRow>> {
253-        self.select_rows(
254-            "subnet = ?1 AND dismissed = 0 ORDER BY created_ms ASC, notif_id ASC",
255-            params![subnet],


120	    /// [`EpochSource`] (`<node_hex>:<epoch>` — unique, replicable), insert it
121	    /// undismissed/unseen, and return the row (the caller hands it to
122	    /// first-fire and/or the replication feed). The epoch is consumed only on
123	    /// a successful insert path — a failed persist returns the error with the
124	    /// counter already durably advanced (a skipped epoch is harmless; a
125	    /// reissued one is not).
126	    // [impl->REQ-NOTIF-1]
127	    #[allow(clippy::too_many_arguments)]
128	    pub fn produce(
129	        &self,
130	        node_hex: &str,
131	        epochs: &mut EpochSource,
132	        subnet: &str,
133	        kind: &str,
134	        from_id: &str,
135	        body: &str,
136	    ) -> rusqlite::Result<NotifRow> {
137	        let epoch = epochs
138	            .next_epoch()
139	            .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
140	        let row = NotifRow {
141	            notif_id: format!("{node_hex}:{epoch}"),
142	            subnet: subnet.to_string(),
143	            kind: kind.to_string(),
144	            from_id: from_id.to_string(),
145	            body: body.to_string(),
146	            created_ms: now_ms(),
147	            dismissed: false,
148	            seen: BTreeSet::new(),
149	            last_surfaced_ms: 0,
150	        };
151	        self.insert_row(&row)?;
152	        Ok(row)
153	    }
154
155	    /// Merge one incoming (replicated) row — the semilattice join, the single
156	    /// apply point for the D8 replication feed. Unknown id ⇒ insert whole;
157	    /// known id ⇒ join the state fields (`dismissed` OR, `seen` union,
158	    /// `last_surfaced_ms` max; identity/content fields keep the stored values
159	    /// — same id, same content by construction). Idempotent and commutative:
160	    /// a replayed or reordered feed converges to the same row.
161	    // [impl->REQ-NOTIF-1]
162	    pub fn merge_row(&self, incoming: &NotifRow) -> rusqlite::Result<MergeOutcome> {
163	        // BEGIN IMMEDIATE: the read-join-write must be one writer transaction
164	        // (two concurrent joins on one node must serialize, not lose updates).
165	        self.conn.execute_batch("BEGIN IMMEDIATE")?;
166	        let outcome = (|| -> rusqlite::Result<MergeOutcome> {
167	            let Some(current) = self.read_row(&incoming.notif_id)? else {
168	                self.insert_row(incoming)?;
169	                return Ok(MergeOutcome::Inserted);

    /// [`EpochSource`] (`<node_hex>:<epoch>` — unique, replicable), insert it
    /// undismissed/unseen, and return the row (the caller hands it to
    /// first-fire and/or the replication feed). The epoch is consumed only on
    /// a successful insert path — a failed persist returns the error with the
    /// counter already durably advanced (a skipped epoch is harmless; a
    /// reissued one is not).
    // [impl->REQ-NOTIF-1]
    #[allow(clippy::too_many_arguments)]
    pub fn produce(
        &self,
        node_hex: &str,
        epochs: &mut EpochSource,
        subnet: &str,
        kind: &str,
        from_id: &str,
        body: &str,
    ) -> rusqlite::Result<NotifRow> {
        let epoch = epochs
            .next_epoch()
            .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
        let row = NotifRow {
            notif_id: format!("{node_hex}:{epoch}"),
            subnet: subnet.to_string(),
            kind: kind.to_string(),
            from_id: from_id.to_string(),
            body: body.to_string(),
            created_ms: now_ms(),
            dismissed: false,
            seen: BTreeSet::new(),
            last_surfaced_ms: 0,
        };
        self.insert_row(&row)?;
        Ok(row)
    }

    /// Merge one incoming (replicated) row — the semilattice join, the single
    /// apply point for the D8 replication feed. Unknown id ⇒ insert whole;
    /// known id ⇒ join the state fields (`dismissed` OR, `seen` union,
    /// `last_surfaced_ms` max; identity/content fields keep the stored values
    /// — same id, same content by construction). Idempotent and commutative:
    /// a replayed or reordered feed converges to the same row.
    // [impl->REQ-NOTIF-1]
    pub fn merge_row(&self, incoming: &NotifRow) -> rusqlite::Result<MergeOutcome> {
        // BEGIN IMMEDIATE: the read-join-write must be one writer transaction
        // (two concurrent joins on one node must serialize, not lose updates).
        self.conn.execute_batch("BEGIN IMMEDIATE")?;
        let outcome = (|| -> rusqlite::Result<MergeOutcome> {
            let Some(current) = self.read_row(&incoming.notif_id)? else {
                self.insert_row(incoming)?;
                return Ok(MergeOutcome::Inserted);


Add lifecycle test for the production glue:


    // [unit->REQ-DAEMON-4] the supervised-crash path runs the SAME ordering as a
    // graceful teardown: the echo-commune (3.3) fires and the perch is torn down
    // when the post-grace recheck confirms the harness owner is still gone.

    // [unit->REQ-INST-4] the daemon-hosted rest edge fires the REAL transition
    // echo commune (run_echo_commune through the manifest summarizer) exactly
    // once per active→resting edge — the final context delta lands in the
    // commune drop dir, where it syncs to whichever instance activates next;
    // the dormant→suspended edge fires no second echo.
    // [unit->REQ-INST-3] and the wake edge re-activates in place: it
    // resurfaces undismissed notifs at the Wake boundary and drops the
    // freshness-pull marker the peer pump consumes.
    #[test]
    fn rest_event_fires_echo_once_and_wake_resurfaces() {
        with_home(|home| {
            let drops = tempfile::tempdir().unwrap();
            let manifest = live_manifest_with_echo(drops.path());
            let host = BrainLifecycle::with_config(&manifest, "doyle", DaemonConfig::default())
                .expect("live host");
            seed_supervised_perch("doyle", std::process::id());

            // Canonical stores the wake effects read: a member subnet + one
            // undismissed notif row doyle has never seen.
            let mut subnets = spt_store::subnet::SubnetStore::load();
            subnets.create_subnet("home").unwrap();
            subnets.save().unwrap();
            let store = spt_store::notif::NotifStore::open().unwrap();
            let mut epochs = spt_store::epoch::EpochSource::load_from(&home.join("test-epochs"));
            store.produce("aa11", &mut epochs, "home", "agent", "oak", "hello").unwrap();

            // active → dormant: the echo commune wrote the final delta.
            let commune = drops.path().join("doyle-commune.md");
            let report = host
                .rest_event(Some("sid-1"), crate::resting::RestEvent::Detach)
                .expect("apply ok")
                .expect("edge");
            assert!(report.echo_fired, "active→dormant fires the transition echo");
            assert!(commune.exists(), "the echo wrote a commune before the flip");
            std::fs::remove_file(&commune).unwrap();

            // dormant → suspended: already resting — no second echo.
            let report = host
                .rest_event(Some("sid-1"), crate::resting::RestEvent::Suspend)
                .expect("apply ok")
                .expect("edge");
            assert!(!report.echo_fired, "resting→resting fires no echo");
            assert!(!commune.exists(), "exactly once per edge");

            // wake: re-activation in place + the catch-up effects.
            let report = host
                .rest_event(Some("sid-1"), crate::resting::RestEvent::Wake)
                .expect("apply ok")
                .expect("edge");
            assert_eq!(report.to, crate::resting::RestState::Active);
            let row = &store.undismissed("home").unwrap()[0];
            assert!(
                row.seen.contains("doyle"),
                "wake resurfaced the undismissed notif at the boundary: {row:?}"
            );
            let marker = spt_store::perch::identity_dir()
                .join(crate::resting::PULL_MARKER_FILE);
            assert!(marker.exists(), "wake dropped the freshness-pull marker for the pump");
        });
    }

    // [unit->REQ-DAEMON-4] the supervised-crash path runs the SAME ordering as a
    // graceful teardown: the echo-commune (3.3) fires and the perch is torn down
    // when the post-grace recheck confirms the harness owner is still gone.


    // [unit->REQ-INST-4] the daemon-hosted rest edge fires the REAL transition
    // echo commune (run_echo_commune through the manifest summarizer) exactly
    // once per active→resting edge — the final context delta lands in the
    // commune drop dir, where it syncs to whichever instance activates next;
    // the dormant→suspended edge fires no second echo.
    // [unit->REQ-INST-3] and the wake edge re-activates in place: it
    // resurfaces undismissed notifs at the Wake boundary and drops the
    // freshness-pull marker the peer pump consumes.
    #[test]
    fn rest_event_fires_echo_once_and_wake_resurfaces() {
        with_home(|home| {
            let drops = tempfile::tempdir().unwrap();
            let manifest = live_manifest_with_echo(drops.path());
            let host = BrainLifecycle::with_config(&manifest, "doyle", DaemonConfig::default())
                .expect("live host");
            seed_supervised_perch("doyle", std::process::id());

            // Canonical stores the wake effects read: a member subnet + one
            // undismissed notif row doyle has never seen.
            let mut subnets = spt_store::subnet::SubnetStore::load();
            subnets.create_subnet("home").unwrap();
            subnets.save().unwrap();
            let store = spt_store::notif::NotifStore::open().unwrap();
            let mut epochs = spt_store::epoch::EpochSource::load_from(&home.join("test-epochs"));
            store.produce("aa11", &mut epochs, "home", "agent", "oak", "hello").unwrap();

            // active → dormant: the echo commune wrote the final delta.
            let commune = drops.path().join("doyle-commune.md");
            let report = host
                .rest_event(Some("sid-1"), crate::resting::RestEvent::Detach)
                .expect("apply ok")
                .expect("edge");
            assert!(report.echo_fired, "active→dormant fires the transition echo");
            assert!(commune.exists(), "the echo wrote a commune before the flip");
            std::fs::remove_file(&commune).unwrap();

            // dormant → suspended: already resting — no second echo.
            let report = host
                .rest_event(Some("sid-1"), crate::resting::RestEvent::Suspend)
                .expect("apply ok")
                .expect("edge");
            assert!(!report.echo_fired, "resting→resting fires no echo");
            assert!(!commune.exists(), "exactly once per edge");

            // wake: re-activation in place + the catch-up effects.
            let report = host
                .rest_event(Some("sid-1"), crate::resting::RestEvent::Wake)
                .expect("apply ok")
                .expect("edge");
            assert_eq!(report.to, crate::resting::RestState::Active);
            let row = &store.undismissed("home").unwrap()[0];
            assert!(
                row.seen.contains("doyle"),
                "wake resurfaced the undismissed notif at the boundary: {row:?}"
            );
            let marker = spt_store::perch::identity_dir()
                .join(crate::resting::PULL_MARKER_FILE);
            assert!(marker.exists(), "wake dropped the freshness-pull marker for the pump");
        });
    }

    // [unit->REQ-DAEMON-4] the supervised-crash path runs the SAME ordering as a
    // graceful teardown: the echo-commune (3.3) fires and the perch is torn down
    // when the post-grace recheck confirms the harness owner is still gone.

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

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

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

+    // commune drop dir, where it syncs to whichever instance activates next;

+    // the dormant→suspended edge fires no second echo.

+    // [unit->REQ-INST-3] and the wake edge re-activates in place: it

+    // resurfaces undismissed notifs at the Wake boundary and drops the

+    // freshness-pull marker the peer pump consumes.

+    fn rest_event_fires_echo_once_and_wake_resurfaces() {

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

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

+            let host = BrainLifecycle::with_config(&manifest, "doyle", DaemonConfig::default())

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

+            // Canonical stores the wake effects read: a member subnet + one

+            // undismissed notif row doyle has never seen.

+            let mut subnets = spt_store::subnet::SubnetStore::load();

+            subnets.create_subnet("home").unwrap();

+            let store = spt_store::notif::NotifStore::open().unwrap();

+            let mut epochs = spt_store::epoch::EpochSource::load_from(&home.join("test-epochs"));

+            store.produce("aa11", &mut epochs, "home", "agent", "oak", "hello").unwrap();

+            // active → dormant: the echo commune wrote the final delta.

+            let commune = drops.path().join("doyle-commune.md");

+                .rest_event(Some("sid-1"), crate::resting::RestEvent::Detach)

+            assert!(report.echo_fired, "active→dormant fires the transition echo");

+            assert!(commune.exists(), "the echo wrote a commune before the flip");

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

+            // dormant → suspended: already resting — no second echo.

+                .rest_event(Some("sid-1"), crate::resting::RestEvent::Suspend)

+            assert!(!report.echo_fired, "resting→resting fires no echo");

+            assert!(!commune.exists(), "exactly once per edge");

+            // wake: re-activation in place + the catch-up effects.

+                .rest_event(Some("sid-1"), crate::resting::RestEvent::Wake)

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

+            let row = &store.undismissed("home").unwrap()[0];

+                row.seen.contains("doyle"),

+                "wake resurfaced the undismissed notif at the boundary: {row:?}"

+            let marker = spt_store::perch::identity_dir()

+                .join(crate::resting::PULL_MARKER_FILE);

+            assert!(marker.exists(), "wake dropped the freshness-pull marker for the pump");


Export new symbols + run tests:


pub use resting::{
    effective_auto_suspend, read_rest, transition, write_rest, RestEvent, RestRecord, RestState,
};

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,
};


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, effective_auto_suspend, 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::{
    effective_auto_suspend, read_rest, transition, write_rest, RestEvent, RestRecord, RestState,
};
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};

-    effective_auto_suspend, read_rest, transition, write_rest, RestEvent, RestRecord, RestState,

+    apply_event, effective_auto_suspend, read_rest, request_freshness_pull, take_freshness_pull,

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


PowerShell

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

Run spt-daemon lib tests


test effect::tests::failed_effect_is_not_recorded_applied ... ok
test result: ok. 129 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.70s


cargo clippy --workspace --all-targets -- -D warnings 2>&1 | Select-Object -Last 3; cargo test -p spt-daemon --test peerloop 2>&1 | Select-String -Pattern "test result:|FAILED" | Select-Object -First 5

Clippy + peerloop E2E


    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.94s

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


Activate REQ-INST-4:


Edit

id = "REQ-INST-4"
title = "active to dormant/suspended fires a transition echo commune"
required_stages = []

id = "REQ-INST-4"
title = "active to dormant/suspended fires a transition echo commune"
required_stages = ["impl", "unit"]  # activated M4-D9-2-2: resting::apply_event fires the on_rest_edge echo hook at every active→(dormant|suspended) edge BEFORE the flip persists (KH 3.3 echo-before-teardown ordering; echo failure is loud but never wedges the instance active — the signoff_with posture); BrainLifecycle::rest_event binds the hook to the real run_echo_commune machinery (fire_echo), so the final context delta lands in the commune drop dir and syncs to whichever instance activates next. Exactly-once per edge by the pure table's idempotence (replayed events are no-edges); resting→resting fires nothing. unit = echo-once-per-edge (pure + real-summarizer lifecycle e2e) + loud-failure-persists + no-echo-on-suspend-from-dormant


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-4"
title = "active to dormant/suspended fires a transition echo commune"
required_stages = []

id = "REQ-INST-4"
title = "active to dormant/suspended fires a transition echo commune"
required_stages = ["impl", "unit"]  # activated M4-D9-2-2: resting::apply_event fires the on_rest_edge echo hook at every active→(dormant|suspended) edge BEFORE the flip persists (KH 3.3 echo-before-teardown ordering; echo failure is loud but never wedges the instance active — the signoff_with posture); BrainLifecycle::rest_event binds the hook to the real run_echo_commune machinery (fire_echo), so the final context delta lands in the commune drop dir and syncs to whichever instance activates next. Exactly-once per edge by the pure table's idempotence (replayed events are no-edges); resting→resting fires nothing. unit = echo-once-per-edge (pure + real-summarizer lifecycle e2e) + loud-failure-persists + no-echo-on-suspend-from-dormant

+required_stages = ["impl", "unit"]  # activated M4-D9-2-2: resting::apply_event fires the on_rest_edge echo hook at every active→(dormant|suspended) edge BEFORE the flip persists (KH 3.3 echo-before-teardown ordering; echo failure is loud but never wedges the instance active — the signoff_with posture); BrainLifecycle::rest_event binds the hook to the real run_echo_commune machinery (fire_echo), so the final context delta lands in the commune drop dir and syncs to whichever instance activates next. Exactly-once per edge by the pure table's idempotence (replayed events are no-edges); resting→resting fires nothing. unit = echo-once-per-edge (pure + real-summarizer lifecycle e2e) + loud-failure-persists + no-echo-on-suspend-from-dormant

title = "Two-tier context sync (live to all, project to same-project)"


PowerShell

traceable-reqs check 2>&1 | Select-String -Pattern "REQ-INST-3|REQ-INST-4|FAIL|missing" | Select-Object -First 10; "EXIT=$LASTEXITCODE"

Traceability check


       A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write
clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)
       Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)
       Stale registry entries degrade to fallback, never hard-fail (4.3)
  [OK] REQ-INST-3  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-INST-4  required: [impl, unit]  stages: -doc +impl +unit -int
EXIT=0


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

Check D9-2-1 CI status


conclusion  databaseId headSha                                  status
----------  ---------- -------                                  ------
success    26950637951 031578398664aa6559552f2dfd0d399928d5684d completed
success    26949967085 cb81e831eabe86dd7da4c64658eb2caf1ca3c99e completed


D9-2-1 CI FINAL green. Full workspace test for D9-2-2, then push:


Updated task #1 status


cargo test --workspace 2>&1 | Select-String -Pattern "FAILED|failures:|panicked at" | Select-Object -First 10; "DONE=$LASTEXITCODE"

Full workspace test, failures only


test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 61 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.63s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.73s
test effect::tests::failed_effect_is_not_recorded_applied ... ok
test result: ok. 129 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.59s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.19s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
DONE=


cargo test --workspace 2>&1 | Select-String -Pattern "^(error|failures:)|FAILED" | Select-Object -First 5; "EXIT=$LASTEXITCODE"

Confirm zero failures across workspace


test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 61 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.80s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.29s
test effect::tests::failed_effect_is_not_recorded_applied ... ok
EXIT=


cargo test --workspace *> $null; "EXIT=$LASTEXITCODE"

Workspace test exit code only


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

Stage D9-2-2 changes


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/peerloop.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-daemon/tests/peerloop.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/lib.rs        |   3 +-
 crates/spt-daemon/src/lifecycle.rs  | 116 +++++++++++++++++
 crates/spt-daemon/src/peerloop.rs   |  14 ++-
 crates/spt-daemon/src/resting.rs    | 244 ++++++++++++++++++++++++++++++++++++
 crates/spt-daemon/tests/peerloop.rs |   2 +
 traceable-reqs.toml                 |   2 +-
 6 files changed, 378 insertions(+), 3 deletions(-)


git commit -m @'
feat(daemon): M4-D9-2-2 transition echo + wake effects (REQ-INST-4 activated)

The resting-state machine's edges now carry their effects:

- resting::apply_event — the transition host: reads the durable record
  (recordless live perch starts Active), resolves the knob chain, runs the
  pure table, and on a real edge fires the effect seams in the KH 3.3
  order: active->(dormant|suspended) runs the transition echo BEFORE the
  flip persists (echo failure is loud but never wedges the instance active
  -- the signoff_with posture); wake (->active) runs the catch-up effects
  after. Exactly-once per edge by the table's idempotence.
- BrainLifecycle::rest_event — the production binding: the echo hook is
  the real run_echo_commune (fire_echo), so the outgoing instance's final
  context delta lands in the commune drop dir and syncs to whichever
  instance activates next; the wake hook resurfaces undismissed notifs at
  the Wake boundary (the D8 deferral, real now that the edge exists) and
  drops the freshness-pull marker.
- peerloop: PumpPaths.pull_marker + take_freshness_pull in the round
  gate — a wake forces one sync round NOW instead of waiting out the
  anti-entropy cadence (catch-up-on-activation latency is the point);
  the take is the remove, so one wake claims exactly one forced round.

KH 7.4 note: rest_event runs on its caller's thread; the daemon does not
yet schedule multiple agents' echo calls from one loop — the multi-agent
fan-out decision lands with D9-2-3's production feed points.

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

Commit and push D9-2-2


[dev-freeform 8a7afd6] feat(daemon): M4-D9-2-2 transition echo + wake effects (REQ-INST-4 activated)
 6 files changed, 378 insertions(+), 3 deletions(-)
To https://github.com/SaberMage/spt-core.git
   0315783..8a7afd6  dev-freeform -> dev-freeform


D9-2-2 pushed. While CI runs — D9-2-3 recon. Dispatch registry arm + manifest resolution: