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 mod registryhost;
pub mod relay;
pub mod resting;

//! `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 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 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};

pub mod reconcile;
pub mod registryhost;
pub mod relay;
+pub mod resting;
pub mod relcache;
pub mod release;
pub mod seedmap;


Edit

pub use relay::Relay;
pub use relcache::ReleaseCache;

pub use relay::Relay;
pub use relcache::ReleaseCache;
pub use resting::{
    effective_auto_suspend, read_rest, transition, write_rest, RestEvent, RestRecord, RestState,
};


pub use relay::Relay;
pub use relcache::ReleaseCache;
pub use resting::{
    effective_auto_suspend, read_rest, transition, write_rest, RestEvent, RestRecord, RestState,
};

//! `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 resting;
pub mod relcache;
pub mod release;
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 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,


pub mod registryhost;
pub mod relay;
pub mod resting;
pub mod relcache;
pub mod release;

pub mod registryhost;
pub mod relay;
pub mod relcache;
pub mod release;
pub mod resting;


pub mod registryhost;
pub mod relay;
pub mod relcache;
pub mod release;
pub mod resting;

//! `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 resting;
pub mod relcache;
pub mod release;
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};

pub mod reconcile;
pub mod registryhost;
pub mod relay;
-pub mod resting;
pub mod relcache;
pub mod release;
+pub mod resting;
pub mod seedmap;
pub mod sync;
pub mod transport;


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.


**7. Activate REQ-INST-3:**


id = "REQ-INST-3"
title = "Dormant (warm) / suspended (cold) resting states"
required_stages = []

id = "REQ-INST-3"
title = "Dormant (warm) / suspended (cold) resting states"
required_stages = ["impl", "unit"]  # activated M4-D9-2: spt-daemon::resting explicit state machine (pure transition table, dormancy-onset auto-suspend anchor, global→node→endpoint knob chain default OFF) + info.json rest_state record + registry Status::Suspended + advertisement follows transitions epoch-bumped. doc = D9-3 (DORMANCY-BUDGET.md locks the warm/cold policy with measured numbers)


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


155	[[requirements]]
156	id = "REQ-EP-5"
157	title = "Concrete shell instantiation model: spawn-mints-instance (vs relink/online), registered-on-node permission + broadcast-is-discovery, per-shell require_approval gate, max_instances_per_owner + over_cap, instance aliasing, discovery scope"
158	required_stages = []
159
160	# ───────────────────────────── Instances ────────────────────────────────────
161	[[requirements]]
162	id = "REQ-INST-1"
163	title = "endpoint ID vs instance split (adapter-agnostic ID)"
164	required_stages = []
165	[[requirements]]
166	id = "REQ-INST-2"
167	title = "Per-node files, synced Psyche mind"
168	required_stages = ["impl", "unit"]  # activated M4-D6a: the mind now lives in the tracked/ two-tier store (spt-store::contextstore over BranchStore), physically separate from never-synced node-local perches/ — spt_live::ingest::route_slices routes commune drops' <live-context>/<project-context> slices into a-<id>/p-<project_id> branch files (untagged body → live, the parser fallback; precedence-guarded 6.5; checkpoint commit per written tier); signoff::write_resume_commune stamps Self provenance per slice through the same router; resume::download_psyche_context composes the two tiers back into the two-slice envelope (pure read, never mints store state; other-project download sees live only). Daemon lifecycle derives project_id from the commune dir's parent (cwd fallback). The cross-node SYNC of these branches = D6c (REQ-NET-3/REQ-INST-5); int = D9 two-host. unit = per-tier routing/no-leak + untagged→live + suppressed-consumed + resume-commune both-tiers + download compose/cross-project-isolation + e2e listen→ingest→live-tier
169	[[requirements]]
170	id = "REQ-INST-3"
171	title = "Dormant (warm) / suspended (cold) resting states"
172	required_stages = []
173	[[requirements]]
174	id = "REQ-INST-4"
175	title = "active to dormant/suspended fires a transition echo commune"
176	required_stages = []
177	[[requirements]]
178	id = "REQ-INST-5"
179	title = "Two-tier context sync (live to all, project to same-project)"
180	required_stages = ["impl", "unit"]  # activated M4-D6c-2: scoping is the pull model + the server-side gate (ADR-0013 — requester names a-<id>/p-<project> refs, no new registry fields). spt-daemon::sync::SyncPolicy.allows: a-<id> served only when ∃ subnet S with the handshake-proven origin pinned in the trust store AND synced(id,S) (the D3e gate finally consumed — membership list + visibility, hidden ⟹ not synced, fail-closed on unconfigured lists); p-<project> only to a trusted origin for a hosted project (honest residual: p- tier is branch-granular, a hidden endpoint's per-project slice rides a hosted project's branch — per-file p- filtering post-v1); machinery refs never served. select_refs mirrors the gate requester-side (symmetric scoping). Server re-checks EVERY requested ref — naming is not entitlement. int = D9 two-host. unit = gate decision table + select_refs scoping + loopback two-daemon two-tier E2E (unsynced/hidden refused server-side, tests/sync.rs)
181	[[requirements]]
182	id = "REQ-INST-6"
183	title = "Deferred messages not delivered to dormant/suspended instances"
184	required_stages = []
185	[[requirements]]
186	id = "REQ-INST-7"
187	title = "Subnet registry + bare-id resolution policy"
188	required_stages = ["impl", "unit"]  # activated M4-D3a: spt-net::net::registry — SubnetRegistry { endpoint_id -> [Instance{node,status,epoch}] }, Status{Active,Dormant,Offline}, serde-roundtrippable, merge_instance keeps distinct-node instances as separate rows. The bare-id *resolution policy* (local -> most-recently-active -> id@node, ambiguity refuses) is D3c (REQ-INST-10). Cross-node replication delivered M4-D4d: spt-net::net::replicate — RegistryUpdate {subnet,endpoint_id,Instance} as NDJSON over broker-owned QUIC streams (framing survives chunk coalescing/splitting; corrupt lines skipped, 4.3 posture), apply_update routes by subnet into merge_instance and drops non-member subnets fail-closed (REQ-INST-13 posture); advertise gates (D3d/D3e) run locally BEFORE emission. int two-host = D9. unit = serde roundtrip + distinct-nodes + newer-epoch-supersedes + wire convergence/lease-over-the-wire (tests/replicate.rs).
189	[[requirements]]
190	id = "REQ-INST-8"
191	title = "Remote-control mode distinct from local operation"
192	required_stages = ["impl", "unit"]  # activated M4-D5b: AttachRecord wire protocol (spt-net::net::attach) + spt-daemon::attach serve/request/input — a byte-stream viewport onto a remote session over broker QUIC streams; compute+files stay on the target node. tests/attach.rs drives a real PTY child on daemon A from daemon B and proves restart survival (worst-case seq-0 re-serve, all journals dedup). The two-host int leg is D9
193	# ── Multi-subnet membership model (ADR-0006, 2026-06-01; activate M4) ──
194	[[requirements]]
195	id = "REQ-INST-9"
196	title = "Multi-subnet membership (same-user N subnets; cross-user seam)"
197	required_stages = ["impl", "unit"]  # activated M4-D3d: SubnetStore already carries N seeds (D2c/f); D3d adds the join-time bare-id collision check — spt_net::net::registry::SubnetRegistry::join_endpoint refuses (JoinCollision, registry untouched) when a joining endpoint id is already held by a DIFFERENT node in the target subnet (Offline rows still own the name); same-node re-advertise falls through to the merge_instance epoch lease. Per-subnet scope: the same bare name in different subnets is legal (resolver forces qualification, REQ-INST-10). Same-user only; cross-user seam = registry key generalizes to (subnet,user) per ADR-0006, check rides along unchanged. unit = fresh-join insert + other-node refuse-untouched + offline-holder collide + same-node lease fall-through + per-subnet legality.
198	[[requirements]]
199	id = "REQ-INST-10"

[[requirements]]
id = "REQ-EP-5"
title = "Concrete shell instantiation model: spawn-mints-instance (vs relink/online), registered-on-node permission + broadcast-is-discovery, per-shell require_approval gate, max_instances_per_owner + over_cap, instance aliasing, discovery scope"
required_stages = []

# ───────────────────────────── Instances ────────────────────────────────────
[[requirements]]
id = "REQ-INST-1"
title = "endpoint ID vs instance split (adapter-agnostic ID)"
required_stages = []
[[requirements]]
id = "REQ-INST-2"
title = "Per-node files, synced Psyche mind"
required_stages = ["impl", "unit"]  # activated M4-D6a: the mind now lives in the tracked/ two-tier store (spt-store::contextstore over BranchStore), physically separate from never-synced node-local perches/ — spt_live::ingest::route_slices routes commune drops' <live-context>/<project-context> slices into a-<id>/p-<project_id> branch files (untagged body → live, the parser fallback; precedence-guarded 6.5; checkpoint commit per written tier); signoff::write_resume_commune stamps Self provenance per slice through the same router; resume::download_psyche_context composes the two tiers back into the two-slice envelope (pure read, never mints store state; other-project download sees live only). Daemon lifecycle derives project_id from the commune dir's parent (cwd fallback). The cross-node SYNC of these branches = D6c (REQ-NET-3/REQ-INST-5); int = D9 two-host. unit = per-tier routing/no-leak + untagged→live + suppressed-consumed + resume-commune both-tiers + download compose/cross-project-isolation + e2e listen→ingest→live-tier
[[requirements]]
id = "REQ-INST-3"
title = "Dormant (warm) / suspended (cold) resting states"
required_stages = []
[[requirements]]
id = "REQ-INST-4"
title = "active to dormant/suspended fires a transition echo commune"
required_stages = []
[[requirements]]
id = "REQ-INST-5"
title = "Two-tier context sync (live to all, project to same-project)"
required_stages = ["impl", "unit"]  # activated M4-D6c-2: scoping is the pull model + the server-side gate (ADR-0013 — requester names a-<id>/p-<project> refs, no new registry fields). spt-daemon::sync::SyncPolicy.allows: a-<id> served only when ∃ subnet S with the handshake-proven origin pinned in the trust store AND synced(id,S) (the D3e gate finally consumed — membership list + visibility, hidden ⟹ not synced, fail-closed on unconfigured lists); p-<project> only to a trusted origin for a hosted project (honest residual: p- tier is branch-granular, a hidden endpoint's per-project slice rides a hosted project's branch — per-file p- filtering post-v1); machinery refs never served. select_refs mirrors the gate requester-side (symmetric scoping). Server re-checks EVERY requested ref — naming is not entitlement. int = D9 two-host. unit = gate decision table + select_refs scoping + loopback two-daemon two-tier E2E (unsynced/hidden refused server-side, tests/sync.rs)
[[requirements]]
id = "REQ-INST-6"
title = "Deferred messages not delivered to dormant/suspended instances"
required_stages = []
[[requirements]]
id = "REQ-INST-7"
title = "Subnet registry + bare-id resolution policy"
required_stages = ["impl", "unit"]  # activated M4-D3a: spt-net::net::registry — SubnetRegistry { endpoint_id -> [Instance{node,status,epoch}] }, Status{Active,Dormant,Offline}, serde-roundtrippable, merge_instance keeps distinct-node instances as separate rows. The bare-id *resolution policy* (local -> most-recently-active -> id@node, ambiguity refuses) is D3c (REQ-INST-10). Cross-node replication delivered M4-D4d: spt-net::net::replicate — RegistryUpdate {subnet,endpoint_id,Instance} as NDJSON over broker-owned QUIC streams (framing survives chunk coalescing/splitting; corrupt lines skipped, 4.3 posture), apply_update routes by subnet into merge_instance and drops non-member subnets fail-closed (REQ-INST-13 posture); advertise gates (D3d/D3e) run locally BEFORE emission. int two-host = D9. unit = serde roundtrip + distinct-nodes + newer-epoch-supersedes + wire convergence/lease-over-the-wire (tests/replicate.rs).
[[requirements]]
id = "REQ-INST-8"
title = "Remote-control mode distinct from local operation"
required_stages = ["impl", "unit"]  # activated M4-D5b: AttachRecord wire protocol (spt-net::net::attach) + spt-daemon::attach serve/request/input — a byte-stream viewport onto a remote session over broker QUIC streams; compute+files stay on the target node. tests/attach.rs drives a real PTY child on daemon A from daemon B and proves restart survival (worst-case seq-0 re-serve, all journals dedup). The two-host int leg is D9
# ── Multi-subnet membership model (ADR-0006, 2026-06-01; activate M4) ──
[[requirements]]
id = "REQ-INST-9"
title = "Multi-subnet membership (same-user N subnets; cross-user seam)"
required_stages = ["impl", "unit"]  # activated M4-D3d: SubnetStore already carries N seeds (D2c/f); D3d adds the join-time bare-id collision check — spt_net::net::registry::SubnetRegistry::join_endpoint refuses (JoinCollision, registry untouched) when a joining endpoint id is already held by a DIFFERENT node in the target subnet (Offline rows still own the name); same-node re-advertise falls through to the merge_instance epoch lease. Per-subnet scope: the same bare name in different subnets is legal (resolver forces qualification, REQ-INST-10). Same-user only; cross-user seam = registry key generalizes to (subnet,user) per ADR-0006, check rides along unchanged. unit = fresh-join insert + other-node refuse-untouched + offline-holder collide + same-node lease fall-through + per-subnet legality.
[[requirements]]
id = "REQ-INST-10"


id = "REQ-INST-3"
title = "Dormant (warm) / suspended (cold) resting states"
required_stages = []

id = "REQ-INST-3"
title = "Dormant (warm) / suspended (cold) resting states"
required_stages = ["impl", "unit"]  # activated M4-D9-2-1: spt-daemon::resting — the explicit daemon-owned state machine (pure transition table: detach/attention-shift rest warm, manual suspend rests cold from either live state, opt-in auto-suspend counted from the dormancy-onset anchor, wake re-activates in place; no idle timer by design) + durable info.json rest_state/dormant_since_ms record (anchor moves atomically with the state) + the global→node→endpoint auto-suspend knob chain default OFF (daemon.json node leg, info.json endpoint leg, 0 = explicit endpoint OFF) + registry Status::Suspended (additive, addressable — only Offline is unroutable) + advertise_local follows the machine epoch-bumped. Transition echo/effects = D9-2-2 (REQ-INST-4); doc (measured warm/cold policy) = D9-3 DORMANCY-BUDGET.md


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-3"
title = "Dormant (warm) / suspended (cold) resting states"
required_stages = []

id = "REQ-INST-3"
title = "Dormant (warm) / suspended (cold) resting states"
required_stages = ["impl", "unit"]  # activated M4-D9-2-1: spt-daemon::resting — the explicit daemon-owned state machine (pure transition table: detach/attention-shift rest warm, manual suspend rests cold from either live state, opt-in auto-suspend counted from the dormancy-onset anchor, wake re-activates in place; no idle timer by design) + durable info.json rest_state/dormant_since_ms record (anchor moves atomically with the state) + the global→node→endpoint auto-suspend knob chain default OFF (daemon.json node leg, info.json endpoint leg, 0 = explicit endpoint OFF) + registry Status::Suspended (additive, addressable — only Offline is unroutable) + advertise_local follows the machine epoch-bumped. Transition echo/effects = D9-2-2 (REQ-INST-4); doc (measured warm/cold policy) = D9-3 DORMANCY-BUDGET.md

+required_stages = ["impl", "unit"]  # activated M4-D9-2-1: spt-daemon::resting — the explicit daemon-owned state machine (pure transition table: detach/attention-shift rest warm, manual suspend rests cold from either live state, opt-in auto-suspend counted from the dormancy-onset anchor, wake re-activates in place; no idle timer by design) + durable info.json rest_state/dormant_since_ms record (anchor moves atomically with the state) + the global→node→endpoint auto-suspend knob chain default OFF (daemon.json node leg, info.json endpoint leg, 0 = explicit endpoint OFF) + registry Status::Suspended (additive, addressable — only Offline is unroutable) + advertise_local follows the machine epoch-bumped. Transition echo/effects = D9-2-2 (REQ-INST-4); doc (measured warm/cold policy) = D9-3 DORMANCY-BUDGET.md

title = "active to dormant/suspended fires a transition echo commune"


Build + test:


PowerShell

cargo test -p spt-daemon resting -p spt-store info -p spt-net registry 2>&1 | Select-Object -Last 5

Run targeted tests for new resting model


Error: Exit code 1
error: unexpected argument 'info' found

Usage: cargo.exe test [OPTIONS] [TESTNAME] [-- [ARGS]...]

For more information, try '--help'.


cargo test -p spt-daemon -p spt-store -p spt-net --lib 2>&1 | Select-Object -Last 25

Run lib tests across touched crates


test visibility::tests::hidden_gates_sync_even_when_listed ... ok
test visibility::tests::hidden_is_or_of_defaults ... ok
test visibility::tests::override_wins_both_directions ... ok
test trust::tests::store_persists_and_degrades_gracefully ... ok
test visibility::tests::synced_requires_membership_listing ... ok
test visibility::tests::store_persists_and_degrades_safe ... ok
test xfer::tests::xfer_id_is_filename_safe_and_distinct ... ok
test xfer::tests::progress_round_trips_per_role_and_updates_mid_flight ... ok
test branchstore::tests::commit_then_read_at_tip ... ok
test branchstore::tests::multi_key_commit_is_one_commit ... ok
test branchstore::tests::unchanged_write_is_no_commit ... ok
test contextstore::tests::project_branch_holds_many_agents ... ok
test contextstore::tests::two_tier_layout_and_commits ... ok
test contextstore::tests::conflict_artifacts_preserve_both_versions ... ok
test contextstore::tests::rename_ripples_both_tiers ... ok
test syncmerge::tests::legacy_bottom_vectors_surface_not_overwrite ... ok
test syncmerge::tests::adopt_then_incremental_then_idempotent_repull ... ok
test syncmerge::tests::project_branch_unions_disjoint_agents ... ok
test syncmerge::tests::dominated_incoming_drops_but_joins_dag ... ok
test syncmerge::tests::concurrent_writes_surface_on_both_nodes ... ok
test syncmerge::tests::reconciled_write_propagates_and_clears_artifacts ... ok
test info::tests::concurrent_read_during_writes_never_torn ... ok

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


cargo test -p spt-daemon -p spt-store -p spt-net --lib 2>&1 | Select-String -Pattern "test result|Running|error" | Select-Object -First 20

Show per-crate test summaries


     Running unittests src\lib.rs (target\debug\deps\spt_daemon-df85ccfb281dc93f.exe)
running 124 tests
test frame::tests::unknown_kind_is_not_a_decode_error ... ok
test daemon::tests::is_running_tracks_the_seed_channel ... ok
test result: ok. 124 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.63s
     Running unittests src\lib.rs (target\debug\deps\spt_net-c31e58fd7f0dfb88.exe)
running 89 tests
test result: ok. 88 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.32s
     Running unittests src\lib.rs (target\debug\deps\spt_store-875539869fc13e17.exe)
running 123 tests
test nodeid::tests::corrupt_key_errors_never_regenerates ... ok
test registry::tests::resolve_on_empty_registry_is_none_not_error ... ok
test result: ok. 123 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.00s


cargo clippy --workspace --all-targets -- -D warnings 2>&1 | Select-Object -Last 8

Clippy deny-warnings across workspace


    Checking spt-term v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
    Checking spt-runtime v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
    Checking spt-net v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
    Checking spt-live v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
    Checking mock-adapter v0.0.0 (C:\Users\decid\Documents\projects\spt-core\adapters\mock)
    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 6.03s


traceable-reqs check 2>&1 | Select-Object -Last 6; "EXIT=$LASTEXITCODE"

Run traceability check from repo root


  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
EXIT=0


Clippy clean, traceability exit 0. Full workspace test before push:


cargo test --workspace 2>&1 | Select-String -Pattern "test result:|FAILED|panicked" | Select-Object -First 40

Full workspace test run


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.62s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.10s
test effect::tests::failed_effect_is_not_recorded_applied ... ok
test result: ok. 124 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.52s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.25s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.21s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.18s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.51s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.20s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.57s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.26s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.23s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.20s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.52s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.61s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.41s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.26s
test reconcile::tests::failed_turn_preserves_everything ... ok
test result: ok. 49 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.20s
test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.39s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.38s
test result: ok. 88 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.20s
test result: ok. 54 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.33s
test result: ok. 123 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.56s
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.01s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


All green. Commit + push D9-2-1:


git add -A && git status --short && git diff --cached --stat | Select-Object -Last 8

Stage and review changes


warning: in the working copy of 'crates/spt-daemon/src/config.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/registryhost.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-net/src/net/registry.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
warning: in the working copy of 'crates/spt-daemon/src/resting.rs', LF will be replaced by CRLF the next time Git touches it
M  crates/spt-daemon/src/config.rs
M  crates/spt-daemon/src/lib.rs
M  crates/spt-daemon/src/registryhost.rs
A  crates/spt-daemon/src/resting.rs
M  crates/spt-net/src/net/registry.rs
M  crates/spt-store/src/info.rs
M  traceable-reqs.toml
 crates/spt-daemon/src/config.rs       |  30 ++++
 crates/spt-daemon/src/lib.rs          |   4 +
 crates/spt-daemon/src/registryhost.rs |  65 ++++++-
 crates/spt-daemon/src/resting.rs      | 311 ++++++++++++++++++++++++++++++++++
 crates/spt-net/src/net/registry.rs    |  26 +++
 crates/spt-store/src/info.rs          |  90 ++++++++++
 traceable-reqs.toml                   |   2 +-
 7 files changed, 525 insertions(+), 3 deletions(-)


git commit -m @'
feat(daemon,store,net): M4-D9-2-1 resting-state model (REQ-INST-3 activated)

The explicit daemon-owned instance state machine behind dormant (warm) /
suspended (cold), per CONTEXT's no-idle-timer model:

- spt-daemon::resting: pure transition table (detach/attention-shift rest
  an active instance warm; manual suspend rests cold from either live
  state; opt-in auto-suspend counted from the dormancy-onset anchor, never
  last activity; wake re-activates in place; event replay is a no-edge by
  construction) + effective_auto_suspend knob chain (global default OFF ->
  daemon.json node leg -> info.json endpoint leg, 0 = explicit OFF).
- info.json grows rest_state + dormant_since_ms (one atomic pair: no
  dormant record without its anchor, no stale anchor elsewhere) +
  auto_suspend_after_ms endpoint override; pre-D9-2 records parse clean.
- Registry Status grows Suspended (additive serde; addressable -- only
  Offline is unroutable, a wake must route to a cold seat).
- RegistryHost::advertise_local follows the machine: live perch with a
  record advertises its state, each transition reaching peers as an
  epoch-bumped update under the lease; recordless/dead perches keep the
  legacy liveness mapping.

Transition echo + wake resurface/freshness-pull effects land in D9-2-2;
doc stage (measured warm/cold policy) is D9-3's DORMANCY-BUDGET.md.

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

Commit and push D9-2-1


[dev-freeform 0315783] feat(daemon,store,net): M4-D9-2-1 resting-state model (REQ-INST-3 activated)
 7 files changed, 525 insertions(+), 3 deletions(-)
 create mode 100644 crates/spt-daemon/src/resting.rs
To https://github.com/SaberMage/spt-core.git
   cb81e83..0315783  dev-freeform -> dev-freeform


Pushed. Watch CI in background while reconning D9-2-2:


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

Check CI run status for pushed commit


conclusion  databaseId headSha                                  status
----------  ---------- -------                                  ------
           26950637951 031578398664aa6559552f2dfd0d399928d5684d queued
success    26949967085 cb81e831eabe86dd7da4c64658eb2caf1ca3c99e completed
success    26949697687 0dd85d5bae817201f4d239d04ddf4a0431e6ec2f completed


C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-70-/// surfaced notif also renders endpoint-native — spawned detached,
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-71-/// best-effort, combinable with the agent-surface delivery.
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-72-// [impl->REQ-NOTIF-2]
crates\spt\src\api\reporting.rs-73-pub(super) fn resurface_notifs_with(id: &str, manifest: Option<&Manifest>) {
crates\spt\src\api\reporting.rs-74-    let Ok(store) = spt_store::notif::NotifStore::open() else { return };
crates\spt\src\api\reporting.rs-75-    let policy = spt_daemon::NotifSurfacePolicy::load();
crates\spt\src\api\reporting.rs-76-    let now_ms = std::time::SystemTime::now()
crates\spt\src\api\reporting.rs-77-        .duration_since(std::time::UNIX_EPOCH)
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-78-        .map(|d| d.as_millis() as u64)
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-79-        .unwrap_or(0);
crates\spt\src\api\reporting.rs:80:    match spt_daemon::resurface_at_boundary(
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-81-        &store,
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-82-        id,
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-83-        &policy,
crates\spt\src\api\reporting.rs-84-        &perch::owlery_dir(),
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-85-        now_ms,
crates\spt\src\api\reporting.rs-86-        spt_daemon::SUPPRESSION_WINDOW_MS,
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-87-    ) {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-88-        Ok(outcomes) => {
crates\spt\src\api\reporting.rs-89-            let surfaced: Vec<&String> = outcomes
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-90-                .iter()
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-91-                .filter_map(|o| match o {
crates\spt\src\api\reporting.rs-92-                    spt_daemon::ResurfaceOutcome::Surfaced { notif_id, .. } => Some(notif_id),
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-93-                    _ => None,
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-94-                })
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-95-                .collect();
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-96-            if !surfaced.is_empty() {
crates\spt\src\api\reporting.rs-97-                eprintln!("NOTIFS_RESURFACED:{id}:{}", surfaced.len());
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-98-            }
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-99-            // The endpoint-native render (opt-in seam).
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-100-            if let Some(m) = manifest.filter(|m| m.session.notif.is_some()) {
crates\spt\src\api\reporting.rs-101-                let runtime = spt_runtime::ManifestRuntime::new(m.clone());
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-102-                for notif_id in surfaced {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs-103-                    if let Ok(Some(row)) = store.get(notif_id) {
crates\spt\src\api\reporting.rs-104-                        match spt_daemon::notif::spawn_notif_command(&runtime, &row) {
crates\spt\src\api\reporting.rs-105-                            Ok(pid) => eprintln!("NOTIF_COMMAND:{notif_id} pid={pid}"),


crates\spt-daemon\tests\peerloop.rs-1-\! Peer-pump E2E (D9-1-2): the subnet self-drives. One in-process "node A"
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-2-//! runs the production pump loop against "node B" running the production
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-3-//! dispatcher — and registry rows, notif rows, and a mind A never held all
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-4-//! converge with NO test code driving a single protocol exchange.
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-5-//!
crates\spt-daemon\tests\peerloop.rs-6-\! Topology: B owns the process's canonical SPT_HOME (its dispatcher's gates
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-7-//! load from it — the production read path); A's pump-side stores live under
crates\spt-daemon\tests\peerloop.rs:8://! explicit test roots ([`PumpPaths`]). The resolver is injected (hermetic
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-9-//! loopback endpoints run no discovery); production resolves by id.
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-10-
crates\spt-daemon\tests\peerloop.rs-11-use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
crates\spt-daemon\tests\peerloop.rs-12-use std::sync::{Arc, Mutex, OnceLock};
crates\spt-daemon\tests\peerloop.rs-13-use std::thread;
crates\spt-daemon\tests\peerloop.rs-14-use std::time::Duration;
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-15-
crates\spt-daemon\tests\peerloop.rs-16-use spt_daemon::brain::Brain;
crates\spt-daemon\tests\peerloop.rs-17-use spt_daemon::broker::Broker;
crates\spt-daemon\tests\peerloop.rs-18-use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};
crates\spt-daemon\tests\peerloop.rs-19-use spt_daemon::nethost::{NetConfig, NetHost};
crates\spt-daemon\tests\peerloop.rs-20-use spt_daemon::peerloop::{
crates\spt-daemon\tests\peerloop.rs:21:    run_peer_pump, PeerResolver, PumpCadence, PumpConfig, PumpHooks, PumpPaths,
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-22-};
crates\spt-daemon\tests\peerloop.rs-23-use spt_daemon::registryhost::{RegistryGatePolicy, RegistryHost};
crates\spt-daemon\tests\peerloop.rs-24-use spt_daemon::relcache::ReleaseCache;
crates\spt-daemon\tests\peerloop.rs-25-use spt_daemon::release::{sha256_hex, ReleaseMetadata, SignedRelease};
crates\spt-daemon\tests\peerloop.rs-26-use spt_daemon::update::BROKER_RESOURCE_ABI;
crates\spt-daemon\tests\peerloop.rs-27-use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\peerloop.rs-28-use spt_net::net::registry::{Instance, Status};
crates\spt-daemon\tests\peerloop.rs-29-use spt_net::net::replicate::RegistryUpdate;
crates\spt-daemon\tests\peerloop.rs-30-use spt_proto::identity::Identity;
crates\spt-daemon\tests\peerloop.rs-31-use spt_store::contextstore::ContextStore;
crates\spt-daemon\tests\peerloop.rs-32-use spt_store::epoch::EpochSource;
crates\spt-daemon\tests\peerloop.rs-33-use spt_store::info::{self, InfoJson};
crates\spt-daemon\tests\peerloop.rs-34-use spt_store::notif::{NotifRow, NotifStore};
crates\spt-daemon\tests\peerloop.rs-35-use spt_store::perch::{self, ParentHint};
crates\spt-daemon\tests\peerloop.rs-36-use spt_store::subnet::SubnetStore;
crates\spt-daemon\tests\peerloop.rs-37-use spt_store::trust::TrustStore;
crates\spt-daemon\tests\peerloop.rs-38-use spt_store::visibility::VisibilityStore;
crates\spt-daemon\tests\peerloop.rs-39-use tempfile::TempDir;
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-40-
crates\spt-daemon\tests\peerloop.rs-41-static HOME: OnceLock<TempDir> = OnceLock::new();
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-165-        dir.path().join("b-registry"),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-166-    ));
crates\spt-daemon\tests\peerloop.rs-167-    let stop = Arc::new(AtomicBool::new(false));
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-168-    {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-169-        let name = b_name.clone();
crates\spt-daemon\tests\peerloop.rs-170-        let reg = Arc::clone(&b_registry);
crates\spt-daemon\tests\peerloop.rs-171-        let flag = Arc::clone(&stop);
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-172-        let paths = DispatchPaths {
crates\spt-daemon\tests\peerloop.rs-173-            owlery: perch::owlery_dir(),
crates\spt-daemon\tests\peerloop.rs-174-            releases: perch::spt_home().join("releases"),
crates\spt-daemon\tests\peerloop.rs-175-            scratch: dir.path().join("b-scratch"),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-176-        };
crates\spt-daemon\tests\peerloop.rs-177-        thread::spawn(move || {
crates\spt-daemon\tests\peerloop.rs-178-            let _ = run_dispatch_loop(&name, &paths, reg, &flag, Duration::from_millis(25));
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-179-        });
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-180-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-181-
crates\spt-daemon\tests\peerloop.rs-182-    \ ── A (explicit roots): a perch to advertise, a notif row to push, an
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-183-    //    empty context store that will receive the bootstrap pull. ────────
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-184-    let a_root = dir.path().join("a");
crates\spt-daemon\tests\peerloop.rs:185:    let a_paths = PumpPaths {
crates\spt-daemon\tests\peerloop.rs-186-        owlery: a_root.join("owlery"),
crates\spt-daemon\tests\peerloop.rs-187-        ctx_root: a_root.join("context"),
crates\spt-daemon\tests\peerloop.rs-188-        notif_db: a_root.join("notifs.db"),
crates\spt-daemon\tests\peerloop.rs-189-        scratch: a_root.join("scratch"),
crates\spt-daemon\tests\peerloop.rs-190-        subnets: a_root.join("subnet.json"),
crates\spt-daemon\tests\peerloop.rs-191-        trust: a_root.join("trust.json"),
crates\spt-daemon\tests\peerloop.rs-192-        vis: a_root.join("visibility.json"),
crates\spt-daemon\tests\peerloop.rs-193-        ops: a_root.join("pump-ops.json"),
crates\spt-daemon\tests\peerloop.rs-194-        releases: a_root.join("releases"),
crates\spt-daemon\tests\peerloop.rs-195-        release_keys: a_root.join("release-keys.json"),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-196-    };
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-197-    // A trusts B's release signer — the update leg's production gate.
crates\spt-daemon\tests\peerloop.rs-198-    std::fs::create_dir_all(&a_root).unwrap();
crates\spt-daemon\tests\peerloop.rs-199-    let keys_json = serde_json::json!({
crates\spt-daemon\tests\peerloop.rs-200-        "keys": { "k": sk.verifying_key().as_bytes().iter()
crates\spt-daemon\tests\peerloop.rs-201-            .map(|b| format!("{b:02x}")).collect::<String>() },
crates\spt-daemon\tests\peerloop.rs-202-        "channel": "stable",
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-203-    });
crates\spt-daemon\tests\peerloop.rs-204-    std::fs::write(&a_paths.release_keys, serde_json::to_string(&keys_json).unwrap()).unwrap();
crates\spt-daemon\tests\peerloop.rs-205-    let perch_path = perch::resolve_perch_path_in(&a_paths.owlery, "pump-ag", ParentHint::Infer);
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-247-            &b_hex,
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-248-            &[RegistryUpdate {
crates\spt-daemon\tests\peerloop.rs-249-                subnet: subnet.to_string(),
crates\spt-daemon\tests\peerloop.rs-250-                endpoint_id: "ling-pumped".to_string(),
crates\spt-daemon\tests\peerloop.rs-251-                instance: Instance { node: b_hex.clone(), status: Status::Active, epoch: 1 },
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-252-            }],
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-253-            &pol,
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-254-        );
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-255-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-256-
crates\spt-daemon\tests\peerloop.rs-257-    \ ── The pump: production loop, injected resolver, fast cadences. ─────
crates\spt-daemon\tests\peerloop.rs-258-    let resolver: PeerResolver = {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-259-        let b_hex = b_hex.clone();
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-260-        let b_addr = b_addr.clone();
crates\spt-daemon\tests\peerloop.rs-261-        Box::new(move |hex| if hex == b_hex { Some(b_addr.clone()) } else { None })
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-262-    };
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-263-    {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-264-        let name = a_name.clone();
crates\spt-daemon\tests\peerloop.rs-265-        let reg = Arc::clone(&a_registry);
crates\spt-daemon\tests\peerloop.rs-266-        let flag = Arc::clone(&stop);
crates\spt-daemon\tests\peerloop.rs:267:        let paths = PumpPaths {
crates\spt-daemon\tests\peerloop.rs-268-            owlery: a_paths.owlery.clone(),
crates\spt-daemon\tests\peerloop.rs-269-            ctx_root: a_paths.ctx_root.clone(),
crates\spt-daemon\tests\peerloop.rs-270-            notif_db: a_paths.notif_db.clone(),
crates\spt-daemon\tests\peerloop.rs-271-            scratch: a_paths.scratch.clone(),
crates\spt-daemon\tests\peerloop.rs-272-            subnets: a_paths.subnets.clone(),
crates\spt-daemon\tests\peerloop.rs-273-            trust: a_paths.trust.clone(),
crates\spt-daemon\tests\peerloop.rs-274-            vis: a_paths.vis.clone(),
crates\spt-daemon\tests\peerloop.rs-275-            ops: a_paths.ops.clone(),
crates\spt-daemon\tests\peerloop.rs-276-            releases: a_paths.releases.clone(),
crates\spt-daemon\tests\peerloop.rs-277-            release_keys: a_paths.release_keys.clone(),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-278-        };
crates\spt-daemon\tests\peerloop.rs-279-        thread::spawn(move || {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-280-            let config = PumpConfig {
crates\spt-daemon\tests\peerloop.rs-281-                cadence: PumpCadence {
crates\spt-daemon\tests\peerloop.rs-282-                    registry: Duration::from_millis(100),
crates\spt-daemon\tests\peerloop.rs-283-                    notif: Duration::from_millis(100),
crates\spt-daemon\tests\peerloop.rs-284-                    sync: Duration::from_millis(100),
crates\spt-daemon\tests\peerloop.rs-285-                    update: Duration::from_millis(100),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-286-                },
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-287-                // Gated default (REQ-UPD-4) — the consent notif is the assert.


Two PumpPaths literals to update. Writing D9-2-2 — first resting.rs apply layer + marker:


Edit

/// One instance's durable resting record.

/// The wake-edge freshness-pull marker file (under `identity/`). A
/// dormant→active transition drops it ([`request_freshness_pull`]); the peer
/// pump consumes it ([`take_freshness_pull`]) and forces a sync round NOW
/// instead of waiting out the anti-entropy cadence — the D9-1 seam the wake
/// edge feeds (a freshly-woken instance pulls the latest mind immediately,
/// CONTEXT §catch-up-on-activation). File-based because the transition can
/// happen in another process (`spt wake`) than the pump's daemon.
pub const PULL_MARKER_FILE: &str = "sync-pull-now";

/// Drop the freshness-pull marker (idempotent — a second wake before the
/// pump's next tick just re-requests the same pull).
// [impl->REQ-INST-3]
pub fn request_freshness_pull(marker_path: &Path) -> std::io::Result<()> {
    if let Some(parent) = marker_path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(marker_path, b"")
}

/// Consume the freshness-pull marker: `true` exactly once per drop (the
/// remove is the take — two pump ticks can never both claim one wake).
// [impl->REQ-INST-3]
pub fn take_freshness_pull(marker_path: &Path) -> bool {
    std::fs::remove_file(marker_path).is_ok()
}

/// What one applied event did: the edge taken and whether the transition
/// echo ran (it fires only on active → resting edges).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EdgeReport {
    pub from: RestState,
    pub to: RestState,
    pub echo_fired: bool,
}

/// Apply one event against a perch's durable resting record — the transition
/// host. Reads the record (a recordless live perch starts `Active` — the
/// machine's production feeds only run for hosted live endpoints), resolves
/// the auto-suspend knob chain, runs the pure table, and on a real edge:
///
/// 1. **active → (dormant|suspended)**: runs `on_rest_edge` — the transition
///    echo commune capturing the outgoing instance's final context delta,
///    BEFORE the state flip persists (the KH 3.3 ordering: echo before any
///    teardown the new state implies). An echo failure is loud but does NOT
///    wedge the instance active — the driver IS gone; the state persists and
///    the error surfaces after (the `signoff_with` posture). [impl->REQ-INST-4]
/// 2. persists the landing state ([`write_rest`] — the dormancy anchor moves
///    with it);
/// 3. **→ active (wake)**: runs `on_wake` — the resurface-at-boundary +
///    freshness-pull effects (re-activation in place catches up immediately).
///
/// `Ok(None)` = no edge (the pure table's idempotence: replaying any event
/// against a state it does not move can never double-fire effects).
// [impl->REQ-INST-3]
pub fn apply_event(
    perch_path: &Path,
    event: RestEvent,
    node_auto_suspend_ms: Option<u64>,
    now_ms: u64,
    on_rest_edge: impl FnOnce() -> Result<(), String>,
    on_wake: impl FnOnce(),
) -> Result<Option<EdgeReport>, String> {
    let rec = info::read_info(perch_path)
        .ok_or_else(|| "info.json absent or unreadable — not a hosted perch".to_string())?;
    let from = rec
        .rest_state
        .as_deref()
        .and_then(RestState::from_tag)
        .unwrap_or(RestState::Active);
    let knob = effective_auto_suspend(node_auto_suspend_ms, rec.auto_suspend_after_ms);
    let Some(to) = transition(from, rec.dormant_since_ms, event, knob) else {
        return Ok(None);
    };

    // The transition echo (REQ-INST-4, KH 3.3): at the active → resting edge,
    // before the flip persists. Failure is captured, not short-circuited —
    // the state change is a fact about the driver, not about the echo.
    // [impl->REQ-INST-4]
    let mut echo_err: Option<String> = None;
    let echo_fired = from == RestState::Active && to != RestState::Active;
    if echo_fired {
        if let Err(e) = on_rest_edge() {
            echo_err = Some(e);
        }
    }

    write_rest(perch_path, to, now_ms).map_err(|e| format!("persist rest state: {e}"))?;

    if to == RestState::Active {
        on_wake();
    }

    if let Some(e) = echo_err {
        return Err(format!("transition echo failed (state persisted): {e}"));
    }
    Ok(Some(EdgeReport { from, to, echo_fired }))
}

/// One instance's durable resting record.


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)

/// The wake-edge freshness-pull marker file (under `identity/`). A
/// dormant→active transition drops it ([`request_freshness_pull`]); the peer
/// pump consumes it ([`take_freshness_pull`]) and forces a sync round NOW
/// instead of waiting out the anti-entropy cadence — the D9-1 seam the wake
/// edge feeds (a freshly-woken instance pulls the latest mind immediately,
/// CONTEXT §catch-up-on-activation). File-based because the transition can
/// happen in another process (`spt wake`) than the pump's daemon.
pub const PULL_MARKER_FILE: &str = "sync-pull-now";

/// Drop the freshness-pull marker (idempotent — a second wake before the
/// pump's next tick just re-requests the same pull).
// [impl->REQ-INST-3]
pub fn request_freshness_pull(marker_path: &Path) -> std::io::Result<()> {
    if let Some(parent) = marker_path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(marker_path, b"")
}

/// Consume the freshness-pull marker: `true` exactly once per drop (the
/// remove is the take — two pump ticks can never both claim one wake).
// [impl->REQ-INST-3]
pub fn take_freshness_pull(marker_path: &Path) -> bool {
    std::fs::remove_file(marker_path).is_ok()
}

/// What one applied event did: the edge taken and whether the transition
/// echo ran (it fires only on active → resting edges).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EdgeReport {
    pub from: RestState,
    pub to: RestState,
    pub echo_fired: bool,
}

/// Apply one event against a perch's durable resting record — the transition
/// host. Reads the record (a recordless live perch starts `Active` — the
/// machine's production feeds only run for hosted live endpoints), resolves
/// the auto-suspend knob chain, runs the pure table, and on a real edge:
///
/// 1. **active → (dormant|suspended)**: runs `on_rest_edge` — the transition
///    echo commune capturing the outgoing instance's final context delta,
///    BEFORE the state flip persists (the KH 3.3 ordering: echo before any
///    teardown the new state implies). An echo failure is loud but does NOT
///    wedge the instance active — the driver IS gone; the state persists and
///    the error surfaces after (the `signoff_with` posture). [impl->REQ-INST-4]
/// 2. persists the landing state ([`write_rest`] — the dormancy anchor moves
///    with it);
/// 3. **→ active (wake)**: runs `on_wake` — the resurface-at-boundary +
///    freshness-pull effects (re-activation in place catches up immediately).
///
/// `Ok(None)` = no edge (the pure table's idempotence: replaying any event
/// against a state it does not move can never double-fire effects).
// [impl->REQ-INST-3]
pub fn apply_event(
    perch_path: &Path,
    event: RestEvent,
    node_auto_suspend_ms: Option<u64>,
    now_ms: u64,
    on_rest_edge: impl FnOnce() -> Result<(), String>,
    on_wake: impl FnOnce(),
) -> Result<Option<EdgeReport>, String> {
    let rec = info::read_info(perch_path)
        .ok_or_else(|| "info.json absent or unreadable — not a hosted perch".to_string())?;
    let from = rec
        .rest_state
        .as_deref()
        .and_then(RestState::from_tag)
        .unwrap_or(RestState::Active);
    let knob = effective_auto_suspend(node_auto_suspend_ms, rec.auto_suspend_after_ms);
    let Some(to) = transition(from, rec.dormant_since_ms, event, knob) else {
        return Ok(None);
    };

    // The transition echo (REQ-INST-4, KH 3.3): at the active → resting edge,
    // before the flip persists. Failure is captured, not short-circuited —
    // the state change is a fact about the driver, not about the echo.
    // [impl->REQ-INST-4]
    let mut echo_err: Option<String> = None;
    let echo_fired = from == RestState::Active && to != RestState::Active;
    if echo_fired {
        if let Err(e) = on_rest_edge() {
            echo_err = Some(e);
        }
    }

    write_rest(perch_path, to, now_ms).map_err(|e| format!("persist rest state: {e}"))?;

    if to == RestState::Active {
        on_wake();
    }

    if let Some(e) = echo_err {
        return Err(format!("transition echo failed (state persisted): {e}"));
    }
    Ok(Some(EdgeReport { from, to, echo_fired }))
}

/// One instance's durable resting record.

endpoint_ms.or(node_ms).filter(|ms| *ms > 0)

+/// The wake-edge freshness-pull marker file (under `identity/`). A

+/// dormant→active transition drops it ([`request_freshness_pull`]); the peer

+/// pump consumes it ([`take_freshness_pull`]) and forces a sync round NOW

+/// instead of waiting out the anti-entropy cadence — the D9-1 seam the wake

+/// edge feeds (a freshly-woken instance pulls the latest mind immediately,

+/// CONTEXT §catch-up-on-activation). File-based because the transition can

+/// happen in another process (`spt wake`) than the pump's daemon.

+pub const PULL_MARKER_FILE: &str = "sync-pull-now";

+/// Drop the freshness-pull marker (idempotent — a second wake before the

+/// pump's next tick just re-requests the same pull).

+pub fn request_freshness_pull(marker_path: &Path) -> std::io::Result<()> {

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

+        std::fs::create_dir_all(parent)?;

+/// Consume the freshness-pull marker: `true` exactly once per drop (the

+/// remove is the take — two pump ticks can never both claim one wake).

+pub fn take_freshness_pull(marker_path: &Path) -> bool {

+    std::fs::remove_file(marker_path).is_ok()

+/// What one applied event did: the edge taken and whether the transition

+/// echo ran (it fires only on active → resting edges).

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

+/// Apply one event against a perch's durable resting record — the transition

+/// host. Reads the record (a recordless live perch starts `Active` — the

+/// machine's production feeds only run for hosted live endpoints), resolves

+/// the auto-suspend knob chain, runs the pure table, and on a real edge:

+/// 1. **active → (dormant|suspended)**: runs `on_rest_edge` — the transition

+///    echo commune capturing the outgoing instance's final context delta,

+///    BEFORE the state flip persists (the KH 3.3 ordering: echo before any

+///    teardown the new state implies). An echo failure is loud but does NOT

+///    wedge the instance active — the driver IS gone; the state persists and

+///    the error surfaces after (the `signoff_with` posture). [impl->REQ-INST-4]

+/// 2. persists the landing state ([`write_rest`] — the dormancy anchor moves

+/// 3. **→ active (wake)**: runs `on_wake` — the resurface-at-boundary +

+///    freshness-pull effects (re-activation in place catches up immediately).

+/// `Ok(None)` = no edge (the pure table's idempotence: replaying any event

+/// against a state it does not move can never double-fire effects).

+    on_rest_edge: impl FnOnce() -> Result<(), String>,

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

+    let rec = info::read_info(perch_path)

+        .ok_or_else(|| "info.json absent or unreadable — not a hosted perch".to_string())?;

+    let knob = effective_auto_suspend(node_auto_suspend_ms, rec.auto_suspend_after_ms);

+    let Some(to) = transition(from, rec.dormant_since_ms, event, knob) else {

+    // The transition echo (REQ-INST-4, KH 3.3): at the active → resting edge,

+    // before the flip persists. Failure is captured, not short-circuited —

+    // the state change is a fact about the driver, not about the echo.

+    let mut echo_err: Option<String> = None;

+    let echo_fired = from == RestState::Active && to != RestState::Active;

+        if let Err(e) = on_rest_edge() {

+    write_rest(perch_path, to, now_ms).map_err(|e| format!("persist rest state: {e}"))?;

+        return Err(format!("transition echo failed (state persisted): {e}"));

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


Updated task #2 status


Add apply-layer tests to resting.rs:


// [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.
    #[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