1	//! `spt-daemon` — the per-machine broker/brain supervisor (M3b).
2	//!
3	//! ADR-0004 splits the logical daemon into two implementation layers so a
4	//! routine self-update swaps the logic without disturbing live endpoints:
5	//!
6	//! - a **broker** (stable kernel) holds *only* the un-transferable resources a
7	//!   live consumer would lose continuity on if the logic restarted — PTY
8	//!   masters (from [`spt_term`]), spawned harness children, accepted/listening
9	//!   sockets — and runs no logic;
10	//! - a **brain** (restartable logic) holds routing, registry, manifest parse,
11	//!   and the re-hosted [`spt_live`] lifecycle, attaching to the broker over a
12	//!   **versioned local IPC** so a *newer brain talks to an older broker*.
13	//!
14	//! **B0 (this commit)** lands the crate's layer position plus that versioned
15	//! IPC seam — the load-bearing contract everything else rides:
16	//!
17	//! - [`frame`] — the forward-compatible [`Envelope`] schema + the version
18	//!   [`Hello`] handshake (KNOWN-HAZARDS 2.3, [`REQ-HAZARD-HANDOFF-ARGV-COMPAT`]);
19	//! - [`codec`] — length-prefixed JSON framing over any [`std::io::Read`]/`Write`;
20	//! - [`transport`] — the OS-split carrier (named pipe / UDS) behind one
21	//!   [`DaemonTransport`] trait, mirroring `spt-term`'s `SessionSurface`.
22	//!
23	//! **B1 (this commit)** adds the **broker kernel** ([`broker`]) — the stable
24	//! half that hosts a real `spt-term` PTY child and relays its output/input over
25	//! the IPC, holding only the ADR-0004 §B resources and no logic — plus the
26	//! broker↔brain message catalog ([`msg`]).
27	//!
28	//! **B3 (this commit)** consolidates the interim no-daemon model into the brain:
29	//! the `api listen` pulse / Psyche / drop-ingest loops become scheduled
30	//! in-process loops ([`lifecycle::BrainLifecycle`]) paced by a durable,
31	//! configurable period ([`config::DaemonConfig`]) instead of the interim 5s
32	//! constant; the only residue left in the harness-hosted session is the thin,
33	//! stateless [`relay::Relay`]. The `spt` binary's `LiveHost` becomes a shim over
34	//! `BrainLifecycle` (re-host, not rewrite) ([`REQ-DAEMON-1`]).
35	//!
36	//! **B5 (this commit)** makes the broker↔brain boundary **exactly-once across a
37	//! brain crash** ([`effect::EffectJournal`]): an idempotent `input` carries a
38	//! durable `op_id`, and the broker dedups at the effect keyed by that id over a
39	//! broker-owned recovery anchor (KNOWN-HAZARDS 7.2, [`REQ-HAZARD-RESTART-IDEMPOTENT`];
40	//! Spike #6's two constraints — broker-owned anchor, dedup-at-effect-by-id —
41	//! instantiated on the real PTY-write boundary).
42	//!
43	//! **C0 (this commit)** opens M3c — the self-update engine ([`update`]): the
44	//! ADR-0004 §A update-class taxonomy ([`UpdateClass`] + [`classify`]) and the
45	//! **brain-only zero-interruption swap** ([`apply_brain_only`], [`REQ-UPD-3`]).
46	//! The brain-only apply *triggers* a handoff over the B2 substrate — snapshot →
47	//! drop → re-attach — so the live PTY child + its output stream survive the logic
48	//! swap untouched; it never re-implements the handoff and never touches a broker
49	//! resource.
50	//!
51	//! **C1 (this commit)** adds the verify-before-handoff gate ([`release`]): a
52	//! release is signature-verified ([`REQ-UPD-2`]) and hardened against rollback
53	//! ([`REQ-HAZARD-UPDATE-ROLLBACK`]) — Ed25519 over signed metadata, key
54	//! trust/revocation, channel pinning, metadata expiry, monotonic version, and a
55	//! SHA-256 binding the signature to the actual binary bytes. [`plan_verified`] is
56	//! the front door: it produces an [`UpdatePlan`] *only* for a release that passes
57	//! every check, so an unverified binary can never reach [`apply_brain_only`].
58	//!
59	//! **C3 (this commit)** conducts the **adapter ripple** ([`adapter_update`]):
60	//! after self-update, spt-core updates each registered adapter via its manifest
61	//! `[update]` avenue ([`REQ-UPD-5`], [`REQ-SEAM-UPDATE`]). `file_pull` payloads
62	//! are verified against the **adapter's own** key (manifest-declared) + SHA-256
63	//! (adapter content signing, ADR-0004 §D); `delegated` updaters are opaque, so
64	//! spt-core delegates the trust only when the adapter attests `self_verifies`,
65	//! else skips them as unverifiable.
66	//!
67	//! **D4b (this commit)** makes broker-held QUIC streams survive brain restarts
68	//! **gaplessly and exactly-once** (Spike #3 invariants A–D as real tests): each
69	//! stream's reads are buffered in a sequenced ring (the B2 `OutputLog`
70	//! discipline) a brain subscribes to with a resume cursor; sends/opens carry
71	//! durable `op_id`s journaled at the broker, so a restarted brain's re-drive
72	//! dedups instead of double-writing the wire.
73	//!
74	//! **D4a (this commit)** moves the node's network presence into the broker
75	//! ([`nethost::NetHost`], ADR-0004 §B): a dedicated tokio runtime hosts the
76	//! iroh endpoint + every accepted/dialed QUIC connection, so a brain restart
77	//! never interrupts a cross-node connection. The brain drives net operations
78	//! (endpoint-status, dial) over new IPC frames in the same forward-compatible
79	//! catalog; an `op_id`-carrying dial is exactly-once through the B5
80	//! [`effect::EffectJournal`]. The endpoint binds as the durable node identity
81	//! (`spt-store`'s `node.key`), with the relay stance from `daemon.json`.
82	//!
83	//! **D5a (this commit)** puts WAN message delivery on the D4 substrate: a
84	//! [`WanMessage`](spt_net::net::wanmsg::WanMessage) NDJSON feed over a
85	//! broker-owned QUIC stream funnels through [`wan::receive_wan`] — the ADR-0009
86	//! access-gate seam ([`access`], handshake-proven origin only, KNOWN-HAZARDS
87	//! 7.5) plus durable op-id dedup (7.2, wire-fed) — into the existing local
88	//! REQ-MSG delivery path.
89	//!
90	//! **D5b (this commit)** adds remote terminal attach ([`attach`], REQ-INST-8):
91	//! a byte-stream viewport onto a local session for an operator daemon on
92	//! another node — session output pumped as sequenced
93	//! [`AttachRecord`](spt_net::net::attach::AttachRecord)s (deduped at the
94	//! operator's render cursor), operator input through the B5 exactly-once PTY
95	//! path, the whole loop gated and restart-survivable purely from the D4/B5
96	//! machinery.
97	//!
98	//! The brain + gapless handoff (B2) and the rest of the M3b task table build on
99	//! this seam. `spt-daemon` is internal supervisor machinery, **not** public SDK
100	//! (R-ARCH-2).
101	//!
102	//! [`Envelope`]: frame::Envelope
103	//! [`Hello`]: frame::Hello
104	//! [`DaemonTransport`]: transport::DaemonTransport
105
106	pub mod access;
107	pub mod adapter_update;
108	pub mod attach;
109	pub mod brain;
110	pub mod broker;
111	pub mod codec;
112	pub mod config;
113	pub mod consent;
114	pub mod daemon;
115	pub mod digest;
116	pub mod digesthub;
117	pub mod dispatch;
118	pub mod effect;
119	pub mod endpoint;
120	pub mod frame;
121	pub mod lifecycle;
122	pub mod msg;
123	pub mod nethost;
124	pub mod notif;
125	pub mod notifsync;
126	pub mod peerloop;
127	pub mod propagate;
128	pub mod psyrelay;
129	pub mod reconcile;
130	pub mod registryhost;
131	pub mod relay;
132	pub mod relcache;
133	pub mod release;
134	pub mod seedmap;
135	pub mod sync;
136	pub mod transport;
137	pub mod update;
138	pub mod wan;
139	pub mod xfer;
140
141	#[cfg(test)]
142	mod test_home;
143
144	pub use access::{access_check, classify_inbound, note_outbound, AccessDecision, InboundClass};
145	pub use attach::{request_attach, send_attach_input, serve_attach, AttachServeOutcome};
146	pub use adapter_update::{
147	    conduct_ripple, plan_adapter_update, AdapterRipple, AdapterUpdateOutcome, FilePullPayload,
148	    SkipReason,
149	};
150	pub use brain::{Brain, BrainState, BrokerEvent};
151	pub use broker::Broker;
152	pub use config::{DaemonConfig, RelayChoice};
153	pub use consent::{decide as decide_consent, most_recently_active, ConsentDecision, ConsentTarget};
154	pub use daemon::{ensure_running, is_running, Daemon};
155	pub use digest::{ingest_chunk, DigestEngine, DigestUpdate};
156	pub use digesthub::{
157	    digest_to_json, follow, pull_snapshot, render_digest, render_update, serve_digest_control,
158	    update_to_json, DigestHub,
159	};
160	pub use effect::{EffectJournal, EffectKey, EffectKind, Outcome};
161	pub use endpoint::{broker_socket_name, daemon_pid_path, digest_socket_name, seed_socket_name};
162	pub use lifecycle::{BrainLifecycle, TickReport};
163	pub use nethost::{NetConfig, NetHost, NET_EFFECT_SESSION};
164	pub use notif::{
165	    first_fire, most_recently_active_visible, produce_and_first_fire, produce_consent_notif,
166	    resurface_at_boundary, FirstFireOutcome, NotifSurfacePolicy, ResurfaceOutcome,
167	    NOTIF_KIND_AGENT, NOTIF_KIND_CONSENT, NOTIF_KIND_PSYCHE, SUPPRESSION_WINDOW_MS,
168	};
169	pub use notifsync::{apply_notif_feed, emit_notif_feed, NotifApplyVerdict, NotifPolicy};
170	pub use propagate::{request_update, serve_update, UpdatePullOutcome, UpdateServeOutcome};
171	pub use psyrelay::{
172	    psyche_turn_and_relay, relay_psyche_outbound, PsycheNotifCtx, RelayOutcome, PSYCHE_BODY_CAP,
173	};
174	pub use relay::Relay;
175	pub use relcache::ReleaseCache;
176	pub use seedmap::{put_seed, take_seed, SeedRegistry};
177	pub use frame::{
178	    accept_hello, Envelope, HandshakeError, Hello, Role, IPC_PROTOCOL_VERSION,
179	    MIN_COMPATIBLE_VERSION,
180	};
181	pub use transport::{DaemonTransport, LocalSocketTransport};
182	pub use release::{
183	    parse_verifying_key, sha256_hex, verify_artifact, verify_metadata, verify_signature,
184	    RejectReason, ReleaseMetadata, SignedRelease, VerifyPolicy,
185	};
186	pub use update::{
187	    apply_brain_only, classify, plan_update, plan_verified, ApplyError, BrokerAbi, ReleaseSpec,
188	    UpdateClass, UpdatePlan, BROKER_RESOURCE_ABI,
189	};
190	pub use sync::{
191	    reconcile_after_sync, request_sync, select_refs, serve_sync, ReconcileWiring, SyncPolicy,
192	    SyncPullReport, SyncServeOutcome,
193	};
194	pub use xfer::{fetch_file, push_file, serve_xfer, XferServeOutcome};
195	pub use wan::{receive_wan, WanOutcome};
196

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


Recon done. Writing D9-2-1 now.

**1. `Status::Suspended` (spt-net):**


Edit

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Status {
    /// Live and currently reachable.
    Active,
    /// Known but idle (e.g. a paused live agent) — addressable, not currently serving.
    Dormant,
    /// Believed down. Retained (not dropped) so a newer-epoch revival or a stale-Active
    /// suppression can be reasoned about; the resolver skips it.
    Offline,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Status {
    /// Live and currently reachable.
    Active,
    /// Known but idle (e.g. a paused live agent) — addressable, not currently serving.
    Dormant,
    /// Resting **cold** (D9-2, REQ-INST-3): the harness session is closed,
    /// resumed-on-wake. Still addressable (`spt wake ling@desktop` must route) —
    /// only `Offline` is unroutable. Additive variant on a pre-release fleet;
    /// the NDJSON decode posture (unknown-kind skip) is the registry record's
    /// forward-compat story.
    // [impl->REQ-INST-3]
    Suspended,
    /// Believed down. Retained (not dropped) so a newer-epoch revival or a stale-Active
    /// suppression can be reasoned about; the resolver skips it.
    Offline,
}


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

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Status {
    /// Live and currently reachable.
    Active,
    /// Known but idle (e.g. a paused live agent) — addressable, not currently serving.
    Dormant,
    /// Believed down. Retained (not dropped) so a newer-epoch revival or a stale-Active
    /// suppression can be reasoned about; the resolver skips it.
    Offline,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Status {
    /// Live and currently reachable.
    Active,
    /// Known but idle (e.g. a paused live agent) — addressable, not currently serving.
    Dormant,
    /// Resting **cold** (D9-2, REQ-INST-3): the harness session is closed,
    /// resumed-on-wake. Still addressable (`spt wake ling@desktop` must route) —
    /// only `Offline` is unroutable. Additive variant on a pre-release fleet;
    /// the NDJSON decode posture (unknown-kind skip) is the registry record's
    /// forward-compat story.
    // [impl->REQ-INST-3]
    Suspended,
    /// Believed down. Retained (not dropped) so a newer-epoch revival or a stale-Active
    /// suppression can be reasoned about; the resolver skips it.
    Offline,
}

+    /// Resting **cold** (D9-2, REQ-INST-3): the harness session is closed,

+    /// resumed-on-wake. Still addressable (`spt wake ling@desktop` must route) —

+    /// only `Offline` is unroutable. Additive variant on a pre-release fleet;

+    /// the NDJSON decode posture (unknown-kind skip) is the registry record's


Add Suspended test to registry.rs tests:


    // [unit->REQ-INST-10] a sole remote live instance resolves; an Offline one is skipped.
    #[test]
    fn resolve_sole_live_remote_and_skips_offline() {

    // [unit->REQ-INST-3] a Suspended instance is resting, not gone: it stays
    // addressable (resolves bare and via @node pin — a wake must route to it)
    // and the variant serde-roundtrips (the on-wire/persisted form).
    #[test]
    fn suspended_is_addressable_and_roundtrips() {
        let reg = reg_with(&[("ling", "desk", Status::Suspended)]);
        assert!(matches!(
            reg.resolve(&addr("ling"), "elsewhere"),
            Resolution::Resolved { instance, .. } if instance.node == "desk"
        ));
        assert!(matches!(
            reg.resolve(&addr("ling@desk"), "elsewhere"),
            Resolution::Resolved { instance, .. } if instance.status == Status::Suspended
        ));
        let json = serde_json::to_string(&reg).unwrap();
        let back: SubnetRegistry = serde_json::from_str(&json).unwrap();
        assert_eq!(reg, back, "Suspended roundtrips");
    }

    // [unit->REQ-INST-10] a sole remote live instance resolves; an Offline one is skipped.
    #[test]
    fn resolve_sole_live_remote_and_skips_offline() {


    // [unit->REQ-INST-3] a Suspended instance is resting, not gone: it stays
    // addressable (resolves bare and via @node pin — a wake must route to it)
    // and the variant serde-roundtrips (the on-wire/persisted form).
    #[test]
    fn suspended_is_addressable_and_roundtrips() {
        let reg = reg_with(&[("ling", "desk", Status::Suspended)]);
        assert!(matches!(
            reg.resolve(&addr("ling"), "elsewhere"),
            Resolution::Resolved { instance, .. } if instance.node == "desk"
        ));
        assert!(matches!(
            reg.resolve(&addr("ling@desk"), "elsewhere"),
            Resolution::Resolved { instance, .. } if instance.status == Status::Suspended
        ));
        let json = serde_json::to_string(&reg).unwrap();
        let back: SubnetRegistry = serde_json::from_str(&json).unwrap();
        assert_eq!(reg, back, "Suspended roundtrips");
    }

    // [unit->REQ-INST-10] a sole remote live instance resolves; an Offline one is skipped.
    #[test]
    fn resolve_sole_live_remote_and_skips_offline() {

assert!(matches!(r, Resolution::Resolved { instance, .. } if instance.node == "here"));

+    // [unit->REQ-INST-3] a Suspended instance is resting, not gone: it stays

+    // addressable (resolves bare and via @node pin — a wake must route to it)

+    // and the variant serde-roundtrips (the on-wire/persisted form).

+    fn suspended_is_addressable_and_roundtrips() {

+        let reg = reg_with(&[("ling", "desk", Status::Suspended)]);

+            reg.resolve(&addr("ling"), "elsewhere"),

+            Resolution::Resolved { instance, .. } if instance.node == "desk"

+            reg.resolve(&addr("ling@desk"), "elsewhere"),

+            Resolution::Resolved { instance, .. } if instance.status == Status::Suspended

+        let json = serde_json::to_string(&reg).unwrap();

+        let back: SubnetRegistry = serde_json::from_str(&json).unwrap();

+        assert_eq!(reg, back, "Suspended roundtrips");


**2. info.json fields + setter (spt-store):**


    /// The node currently remote-driving this endpoint's session (hex node id),
    /// stamped by the daemon when it serves a net-attached surface and cleared
    /// when the viewport ends (REQ-REACH-1: detection is a daemon fact, not an
    /// agent guess). Absent ⇒ locally driven (or never net-attached). `default`
    /// so a pre-D5c record (no field) still parses.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub driven_by: Option<String>,
}

    /// The node currently remote-driving this endpoint's session (hex node id),
    /// stamped by the daemon when it serves a net-attached surface and cleared
    /// when the viewport ends (REQ-REACH-1: detection is a daemon fact, not an
    /// agent guess). Absent ⇒ locally driven (or never net-attached). `default`
    /// so a pre-D5c record (no field) still parses.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub driven_by: Option<String>,
    /// The daemon-owned resting state (D9-2, REQ-INST-3): `"active"` /
    /// `"dormant"` / `"suspended"` — the explicit instance state machine's
    /// durable record (`spt_daemon::resting` owns the transitions; this field
    /// is only ever written through [`set_rest_state`]). Distinct from
    /// `status`: `status` is daemon **liveness** (online/offline, KH 2.5);
    /// `rest_state` is the *resting model* of a live instance. Absent ⇒ a
    /// pre-D9-2 / interim record (readers fall back to liveness-derived
    /// status). `default` so older records still parse.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rest_state: Option<String>,
    /// When the instance entered dormancy (epoch ms) — the anchor the opt-in
    /// `auto-suspend-after` threshold counts from (REQ-INST-3: counted from
    /// dormancy onset, never from last activity). Present iff `rest_state`
    /// is `"dormant"`; cleared on every other transition.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dormant_since_ms: Option<u64>,
    /// Per-endpoint `auto-suspend-after` override (ms) — the endpoint leg of
    /// the global → node → endpoint knob chain (REQ-INST-3). Absent ⇒ inherit
    /// the node knob (`daemon.json`); `0` ⇒ explicitly OFF for this endpoint
    /// even when the node enables it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub auto_suspend_after_ms: Option<u64>,
}


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

    /// The node currently remote-driving this endpoint's session (hex node id),
    /// stamped by the daemon when it serves a net-attached surface and cleared
    /// when the viewport ends (REQ-REACH-1: detection is a daemon fact, not an
    /// agent guess). Absent ⇒ locally driven (or never net-attached). `default`
    /// so a pre-D5c record (no field) still parses.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub driven_by: Option<String>,
}

    /// The node currently remote-driving this endpoint's session (hex node id),
    /// stamped by the daemon when it serves a net-attached surface and cleared
    /// when the viewport ends (REQ-REACH-1: detection is a daemon fact, not an
    /// agent guess). Absent ⇒ locally driven (or never net-attached). `default`
    /// so a pre-D5c record (no field) still parses.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub driven_by: Option<String>,
    /// The daemon-owned resting state (D9-2, REQ-INST-3): `"active"` /
    /// `"dormant"` / `"suspended"` — the explicit instance state machine's
    /// durable record (`spt_daemon::resting` owns the transitions; this field
    /// is only ever written through [`set_rest_state`]). Distinct from
    /// `status`: `status` is daemon **liveness** (online/offline, KH 2.5);
    /// `rest_state` is the *resting model* of a live instance. Absent ⇒ a
    /// pre-D9-2 / interim record (readers fall back to liveness-derived
    /// status). `default` so older records still parse.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rest_state: Option<String>,
    /// When the instance entered dormancy (epoch ms) — the anchor the opt-in
    /// `auto-suspend-after` threshold counts from (REQ-INST-3: counted from
    /// dormancy onset, never from last activity). Present iff `rest_state`
    /// is `"dormant"`; cleared on every other transition.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dormant_since_ms: Option<u64>,
    /// Per-endpoint `auto-suspend-after` override (ms) — the endpoint leg of
    /// the global → node → endpoint knob chain (REQ-INST-3). Absent ⇒ inherit
    /// the node knob (`daemon.json`); `0` ⇒ explicitly OFF for this endpoint
    /// even when the node enables it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub auto_suspend_after_ms: Option<u64>,
}

+    /// The daemon-owned resting state (D9-2, REQ-INST-3): `"active"` /

+    /// `"dormant"` / `"suspended"` — the explicit instance state machine's

+    /// durable record (`spt_daemon::resting` owns the transitions; this field

+    /// is only ever written through [`set_rest_state`]). Distinct from

+    /// `status`: `status` is daemon **liveness** (online/offline, KH 2.5);

+    /// `rest_state` is the *resting model* of a live instance. Absent ⇒ a

+    /// pre-D9-2 / interim record (readers fall back to liveness-derived

+    /// status). `default` so older records still parse.

+    #[serde(default, skip_serializing_if = "Option::is_none")]

+    /// When the instance entered dormancy (epoch ms) — the anchor the opt-in

+    /// `auto-suspend-after` threshold counts from (REQ-INST-3: counted from

+    /// dormancy onset, never from last activity). Present iff `rest_state`

+    /// is `"dormant"`; cleared on every other transition.

+    /// Per-endpoint `auto-suspend-after` override (ms) — the endpoint leg of

+    /// the global → node → endpoint knob chain (REQ-INST-3). Absent ⇒ inherit

+    /// the node knob (`daemon.json`); `0` ⇒ explicitly OFF for this endpoint

+    pub auto_suspend_after_ms: Option<u64>,


            parent_pid: None,
            cwd: None,
            status: None,
            last_active_ms: None,
            driven_by: None,
        }
    }
}

            parent_pid: None,
            cwd: None,
            status: None,
            last_active_ms: None,
            driven_by: None,
            rest_state: None,
            dormant_since_ms: None,
            auto_suspend_after_ms: None,
        }
    }
}


            parent_pid: None,
            cwd: None,
            status: None,
            last_active_ms: None,
            driven_by: None,
        }
    }
}

            parent_pid: None,
            cwd: None,
            status: None,
            last_active_ms: None,
            driven_by: None,
            rest_state: None,
            dormant_since_ms: None,
            auto_suspend_after_ms: None,
        }
    }
}

+            auto_suspend_after_ms: None,


/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker on an

/// Stamp the daemon-owned resting state (D9-2, REQ-INST-3) on an existing
/// perch's `info.json` — the state machine's one durable write. `dormant_since_ms`
/// is stamped alongside (the auto-suspend anchor, counted from dormancy onset)
/// and MUST be `Some` exactly when `state` is dormant — the pair moves
/// atomically so a reader never sees a dormant record without its anchor or a
/// stale anchor on a non-dormant one. Read-modify-write through the atomic
/// path. Errors if the record is absent/unreadable — the daemon only stamps
/// perches it hosts.
// [impl->REQ-INST-3]
pub fn set_rest_state(
    perch_path: &Path,
    state: &str,
    dormant_since_ms: Option<u64>,
) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(std::io::ErrorKind::NotFound, "info.json absent or unreadable")
    })?;
    rec.rest_state = Some(state.to_string());
    rec.dormant_since_ms = dormant_since_ms;
    write_info(perch_path, &rec)
}

/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker on an


/// Stamp the daemon-owned resting state (D9-2, REQ-INST-3) on an existing
/// perch's `info.json` — the state machine's one durable write. `dormant_since_ms`
/// is stamped alongside (the auto-suspend anchor, counted from dormancy onset)
/// and MUST be `Some` exactly when `state` is dormant — the pair moves
/// atomically so a reader never sees a dormant record without its anchor or a
/// stale anchor on a non-dormant one. Read-modify-write through the atomic
/// path. Errors if the record is absent/unreadable — the daemon only stamps
/// perches it hosts.
// [impl->REQ-INST-3]
pub fn set_rest_state(
    perch_path: &Path,
    state: &str,
    dormant_since_ms: Option<u64>,
) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(std::io::ErrorKind::NotFound, "info.json absent or unreadable")
    })?;
    rec.rest_state = Some(state.to_string());
    rec.dormant_since_ms = dormant_since_ms;
    write_info(perch_path, &rec)
}

/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker on an

+/// Stamp the daemon-owned resting state (D9-2, REQ-INST-3) on an existing

+/// perch's `info.json` — the state machine's one durable write. `dormant_since_ms`

+/// is stamped alongside (the auto-suspend anchor, counted from dormancy onset)

+/// and MUST be `Some` exactly when `state` is dormant — the pair moves

+/// atomically so a reader never sees a dormant record without its anchor or a

+/// stale anchor on a non-dormant one. Read-modify-write through the atomic

+/// path. Errors if the record is absent/unreadable — the daemon only stamps

+    let mut rec = read_info(perch_path).ok_or_else(|| {

+        std::io::Error::new(std::io::ErrorKind::NotFound, "info.json absent or unreadable")

+    rec.rest_state = Some(state.to_string());

+    rec.dormant_since_ms = dormant_since_ms;

/// existing perch's `info.json` (REQ-REACH-1): the daemon records which node's

/// operator is driving this endpoint's session while it serves a net-attached


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.


Fix struct literal in existing test + add unit test:


            parent_pid: None,
            cwd: None,
            status: None,
            last_active_ms: None,
            driven_by: None,
        };
        write_info(d.path(), &info).unwrap();

            parent_pid: None,
            cwd: None,
            status: None,
            last_active_ms: None,
            driven_by: None,
            rest_state: None,
            dormant_since_ms: None,
            auto_suspend_after_ms: None,
        };
        write_info(d.path(), &info).unwrap();


            parent_pid: None,
            cwd: None,
            status: None,
            last_active_ms: None,
            driven_by: None,
        };
        write_info(d.path(), &info).unwrap();

            parent_pid: None,
            cwd: None,
            status: None,
            last_active_ms: None,
            driven_by: None,
            rest_state: None,
            dormant_since_ms: None,
            auto_suspend_after_ms: None,
        };
        write_info(d.path(), &info).unwrap();


// [unit->REQ-REACH-1] set_driven_by stamps the remote-drive marker onto an

    // [unit->REQ-INST-3] set_rest_state stamps the resting record (state +
    // dormancy anchor as one atomic pair) onto an existing perch: absent by
    // default (pre-D9-2 records parse with rest_state == None), the anchor
    // rides only the dormant state, and a missing record errors.
    #[test]
    fn set_rest_state_stamps_state_and_anchor_as_a_pair() {
        let d = perch();
        write_info(d.path(), &InfoJson::new("ling", "t", 4242, "sid", "live_agent")).unwrap();
        let raw = std::fs::read_to_string(d.path().join("info.json")).unwrap();
        assert!(!raw.contains("rest_state"), "interim perch carries no resting record: {raw}");

        // Dormant carries its auto-suspend anchor.
        set_rest_state(d.path(), "dormant", Some(1_000)).unwrap();
        let rec = read_info(d.path()).unwrap();
        assert_eq!(rec.rest_state.as_deref(), Some("dormant"));
        assert_eq!(rec.dormant_since_ms, Some(1_000));

        // Waking clears the anchor in the SAME write — no stale anchor on a
        // non-dormant record.
        set_rest_state(d.path(), "active", None).unwrap();
        let rec = read_info(d.path()).unwrap();
        assert_eq!(rec.rest_state.as_deref(), Some("active"));
        assert_eq!(rec.dormant_since_ms, None, "anchor cleared on leaving dormancy");

        // No record → error, never a silent write.
        let empty = perch();
        assert!(set_rest_state(empty.path(), "dormant", Some(1)).is_err());

        // Forward tolerance: a pre-D9-2 record (no fields) parses clean.
        let old = perch();
        atomic_write_string(
            &old.path().join("info.json"),
            r#"{"id":"ling","started":"t","pid":1,"session_id":"sid","state":"live_agent"}"#,
        )
        .unwrap();
        let rec = read_info(old.path()).unwrap();
        assert_eq!(rec.rest_state, None);
        assert_eq!(rec.auto_suspend_after_ms, None);
    }

    // [unit->REQ-REACH-1] set_driven_by stamps the remote-drive marker onto an


    // [unit->REQ-INST-3] set_rest_state stamps the resting record (state +
    // dormancy anchor as one atomic pair) onto an existing perch: absent by
    // default (pre-D9-2 records parse with rest_state == None), the anchor
    // rides only the dormant state, and a missing record errors.
    #[test]
    fn set_rest_state_stamps_state_and_anchor_as_a_pair() {
        let d = perch();
        write_info(d.path(), &InfoJson::new("ling", "t", 4242, "sid", "live_agent")).unwrap();
        let raw = std::fs::read_to_string(d.path().join("info.json")).unwrap();
        assert!(!raw.contains("rest_state"), "interim perch carries no resting record: {raw}");

        // Dormant carries its auto-suspend anchor.
        set_rest_state(d.path(), "dormant", Some(1_000)).unwrap();
        let rec = read_info(d.path()).unwrap();
        assert_eq!(rec.rest_state.as_deref(), Some("dormant"));
        assert_eq!(rec.dormant_since_ms, Some(1_000));

        // Waking clears the anchor in the SAME write — no stale anchor on a
        // non-dormant record.
        set_rest_state(d.path(), "active", None).unwrap();
        let rec = read_info(d.path()).unwrap();
        assert_eq!(rec.rest_state.as_deref(), Some("active"));
        assert_eq!(rec.dormant_since_ms, None, "anchor cleared on leaving dormancy");

        // No record → error, never a silent write.
        let empty = perch();
        assert!(set_rest_state(empty.path(), "dormant", Some(1)).is_err());

        // Forward tolerance: a pre-D9-2 record (no fields) parses clean.
        let old = perch();
        atomic_write_string(
            &old.path().join("info.json"),
            r#"{"id":"ling","started":"t","pid":1,"session_id":"sid","state":"live_agent"}"#,
        )
        .unwrap();
        let rec = read_info(old.path()).unwrap();
        assert_eq!(rec.rest_state, None);
        assert_eq!(rec.auto_suspend_after_ms, None);
    }

    // [unit->REQ-REACH-1] set_driven_by stamps the remote-drive marker onto an

+    // [unit->REQ-INST-3] set_rest_state stamps the resting record (state +

+    // default (pre-D9-2 records parse with rest_state == None), the anchor

+    // rides only the dormant state, and a missing record errors.

+    fn set_rest_state_stamps_state_and_anchor_as_a_pair() {

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

+        let raw = std::fs::read_to_string(d.path().join("info.json")).unwrap();

+        assert!(!raw.contains("rest_state"), "interim perch carries no resting record: {raw}");

+        // Dormant carries its auto-suspend anchor.

+        set_rest_state(d.path(), "dormant", Some(1_000)).unwrap();

+        let rec = read_info(d.path()).unwrap();

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

+        assert_eq!(rec.dormant_since_ms, Some(1_000));

+        // Waking clears the anchor in the SAME write — no stale anchor on a

+        set_rest_state(d.path(), "active", None).unwrap();

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

+        assert_eq!(rec.dormant_since_ms, None, "anchor cleared on leaving dormancy");

+        // No record → error, never a silent write.

+        assert!(set_rest_state(empty.path(), "dormant", Some(1)).is_err());

+        // Forward tolerance: a pre-D9-2 record (no fields) parses clean.

+            &old.path().join("info.json"),

+            r#"{"id":"ling","started":"t","pid":1,"session_id":"sid","state":"live_agent"}"#,

+        let rec = read_info(old.path()).unwrap();

+        assert_eq!(rec.rest_state, None);

+        assert_eq!(rec.auto_suspend_after_ms, None);

// existing record (absent by default = locally driven), clears it with

// None, errors on a missing record — and a pre-D5c record (no field at


**3. `resting.rs` — the pure state machine:**


Write

//! The resting-state model (D9-2, REQ-INST-3) — the explicit, daemon-owned
//! instance state machine behind dormant (warm) / suspended (cold).
//!
//! CONTEXT §dormant/suspended defines the model this module implements:
//!
//! - **active** = the most-recently-interacted instance for the ID *and* a
//!   driver attached. **No idle timer** — a session stays active while it
//!   holds both properties.
//! - **active → dormant**: the driver detaches, or attention shifts (another
//!   instance of the same id becomes the most-recently-interacted one).
//! - **dormant → suspended**: manual (`spt suspend` / shell
//!   `api owner-shutdown`), or opt-in auto-suspend after an
//!   `auto-suspend-after` threshold counted **from dormancy onset** — never
//!   from last activity (the [`InfoJson::dormant_since_ms`] anchor).
//! - **wake (→ active)**: re-activation in place — the state is already
//!   there; wake is an edge, not a spawn.
//!
//! The transition function is **pure** ([`transition`]): state + event +
//! resolved knob in, the new state out (`None` = no edge — the event does not
//! move this state). Durability is one field pair on the perch's `info.json`
//! ([`read_rest`] / [`write_rest`] over [`spt_store::info::set_rest_state`]);
//! effects (the transition echo commune, the wake resurface + freshness pull
//! — REQ-INST-4) hang off the *edges* and are wired by the transition host,
//! not here. Keeping the table pure is what makes the CONTEXT state diagram
//! a unit test instead of a prose promise.
//!
//! ## The auto-suspend knob chain (global → node → endpoint, default OFF)
//!
//! Auto-suspend is **opt-in, default OFF globally** (the compiled default:
//! no knob anywhere ⇒ `None` ⇒ never auto-suspends). A node may enable it
//! (`daemon.json` `auto_suspend_after_ms` — a constrained node defaults it
//! ON); an endpoint may override the node either way
//! (`info.json` `auto_suspend_after_ms`, with `0` = explicitly OFF even when
//! the node enables it). [`effective_auto_suspend`] resolves the chain.
//!
//! ## What "suspended" does NOT do here
//!
//! Suspension's session mechanics (closing the harness seat, resume-on-wake)
//! ride the existing live-agent machinery; this module owns the **state
//! model** those mechanics consult. The deferred-message instance-state gate
//! (REQ-INST-6 — deferred rows hold while the instance rests) is **M5**: the
//! state machine here is its precondition, the gate itself stays unbuilt
//! (`required_stages = []`, the documented seam).
// [impl->REQ-INST-3]

use std::path::Path;

use spt_store::info;

/// The resting states (CONTEXT §dormant/suspended): the registry's
/// `Status::Offline` is deliberately NOT here — offline means *node
/// unreachable*, a fact about the node observed by peers, never a state this
/// machine transitions an instance into.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RestState {
    /// Most-recently-interacted for the id + a driver attached.
    Active,
    /// Resting warm: the harness session stays running — instant
    /// re-activation. The default resting state.
    Dormant,
    /// Resting cold: the harness session is closed, resumed on wake.
    Suspended,
}

impl RestState {
    /// The durable `info.json` tag.
    pub fn as_tag(self) -> &'static str {
        match self {
            RestState::Active => "active",
            RestState::Dormant => "dormant",
            RestState::Suspended => "suspended",
        }
    }

    /// Parse a durable tag (`None` for an unknown/foreign value — a newer
    /// fleet's tag must degrade to "no resting record", never wedge a reader).
    pub fn from_tag(tag: &str) -> Option<RestState> {
        match tag {
            "active" => Some(RestState::Active),
            "dormant" => Some(RestState::Dormant),
            "suspended" => Some(RestState::Suspended),
            _ => None,
        }
    }
}

/// The events that can move an instance between resting states. Each names a
/// real observation point in the daemon (the production feeds), not a poll:
/// the model has **no idle timer** by design.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RestEvent {
    /// The driver detached (viewport ended, harness seat released).
    Detach,
    /// Another instance of the same id became the most-recently-interacted
    /// one (CONTEXT: driving `ling@laptop` makes `ling@desktop` dormant).
    AttentionShift,
    /// Manual suspend (`spt suspend` / shell `api owner-shutdown`).
    Suspend,
    /// The daemon's cadence check against the opt-in auto-suspend threshold.
    /// Carries the clock so the table stays pure.
    AutoSuspendTick { now_ms: u64 },
    /// A driver attached / `spt wake` / interaction — re-activate in place.
    Wake,
}

/// The pure transition table. `dormant_since_ms` is the dormancy-onset anchor
/// (present iff `state` is dormant); `auto_suspend_after_ms` is the RESOLVED
/// knob ([`effective_auto_suspend`] — `None` = OFF). Returns the new state on
/// a real edge, `None` when the event does not move this state (idempotent
/// re-delivery of any event is a no-op by construction — an event-feed replay
/// can never double-fire an edge's effects).
// [impl->REQ-INST-3]
pub fn transition(
    state: RestState,
    dormant_since_ms: Option<u64>,
    event: RestEvent,
    auto_suspend_after_ms: Option<u64>,
) -> Option<RestState> {
    match (state, event) {
        // active → dormant: the driver detaches or attention shifts.
        (RestState::Active, RestEvent::Detach) => Some(RestState::Dormant),
        (RestState::Active, RestEvent::AttentionShift) => Some(RestState::Dormant),
        // Manual suspend works from active too (the user said cold — the
        // active→suspended edge still fires the transition echo, KH 3.3).
        (RestState::Active, RestEvent::Suspend) => Some(RestState::Suspended),
        (RestState::Dormant, RestEvent::Suspend) => Some(RestState::Suspended),
        // Opt-in auto-suspend: threshold counted from DORMANCY ONSET. A
        // dormant record without its anchor (torn legacy state) never
        // auto-suspends — fail toward warm, the cheap mistake.
        (RestState::Dormant, RestEvent::AutoSuspendTick { now_ms }) => {
            let after = auto_suspend_after_ms.filter(|ms| *ms > 0)?;
            let since = dormant_since_ms?;
            (now_ms.saturating_sub(since) >= after).then_some(RestState::Suspended)
        }
        // wake = re-activation in place, from either resting state.
        (RestState::Dormant, RestEvent::Wake) => Some(RestState::Active),
        (RestState::Suspended, RestEvent::Wake) => Some(RestState::Active),
        // Everything else: no edge. Notably — Wake on an already-active
        // instance (just a recency stamp, not a transition), Detach /
        // AttentionShift on a resting one, Suspend on suspended (idempotent),
        // AutoSuspendTick anywhere but dormant.
        _ => None,
    }
}

/// Resolve the auto-suspend knob chain (REQ-INST-3): global default **OFF**
/// (`None`), node knob from `daemon.json`, endpoint override from the perch's
/// `info.json` — `Some(0)` at either level is an explicit OFF (an endpoint
/// can opt out of a node that defaults ON). Endpoint wins when present.
// [impl->REQ-INST-3]
pub fn effective_auto_suspend(node_ms: Option<u64>, endpoint_ms: Option<u64>) -> Option<u64> {
    endpoint_ms.or(node_ms).filter(|ms| *ms > 0)
}

/// One instance's durable resting record.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RestRecord {
    pub state: RestState,
    /// Dormancy-onset anchor (present iff `state` is dormant).
    pub dormant_since_ms: Option<u64>,
    /// The endpoint leg of the auto-suspend knob chain.
    pub auto_suspend_after_ms: Option<u64>,
}

/// Read a perch's resting record. `None` ⇒ no record (a pre-D9-2 / interim
/// perch, or an unreadable `info.json`) — callers fall back to the legacy
/// liveness-derived status, exactly what they did before this slice.
// [impl->REQ-INST-3]
pub fn read_rest(perch_path: &Path) -> Option<RestRecord> {
    let rec = info::read_info(perch_path)?;
    let state = RestState::from_tag(rec.rest_state.as_deref()?)?;
    Some(RestRecord {
        state,
        dormant_since_ms: rec.dormant_since_ms,
        auto_suspend_after_ms: rec.auto_suspend_after_ms,
    })
}

/// Persist a transition's landing state. Stamps the dormancy anchor when the
/// new state is dormant (the auto-suspend clock starts at the edge) and
/// clears it otherwise — one atomic write, so no reader ever sees a dormant
/// record without its anchor.
// [impl->REQ-INST-3]
pub fn write_rest(perch_path: &Path, state: RestState, now_ms: u64) -> std::io::Result<()> {
    let anchor = (state == RestState::Dormant).then_some(now_ms);
    info::set_rest_state(perch_path, state.as_tag(), anchor)
}

#[cfg(test)]
mod tests {
    use super::*;
    use spt_store::info::{write_info, InfoJson};

    // [unit->REQ-INST-3] THE transition table — the CONTEXT state diagram as
    // assertions: detach and attention-shift rest an active instance warm,
    // suspend rests it cold from either live state, wake re-activates in
    // place from either resting state, and everything else is a no-edge.
    #[test]
    fn transition_table_matches_the_context_model() {
        use RestEvent::*;
        use RestState::*;

        // active → dormant on detach / attention shift.
        assert_eq!(transition(Active, None, Detach, None), Some(Dormant));
        assert_eq!(transition(Active, None, AttentionShift, None), Some(Dormant));
        // manual suspend from active AND from dormant.
        assert_eq!(transition(Active, None, Suspend, None), Some(Suspended));
        assert_eq!(transition(Dormant, Some(5), Suspend, None), Some(Suspended));
        // wake re-activates from either resting state.
        assert_eq!(transition(Dormant, Some(5), Wake, None), Some(Active));
        assert_eq!(transition(Suspended, None, Wake, None), Some(Active));

        // No-edges: events are idempotent against a state they don't move —
        // a replayed feed can never double-fire an edge.
        assert_eq!(transition(Active, None, Wake, None), None, "already active");
        assert_eq!(transition(Dormant, Some(5), Detach, None), None);
        assert_eq!(transition(Dormant, Some(5), AttentionShift, None), None);
        assert_eq!(transition(Suspended, None, Suspend, None), None, "suspend is idempotent");
        assert_eq!(transition(Suspended, None, Detach, None), None);
        assert_eq!(transition(Suspended, None, AttentionShift, None), None);
    }

    // [unit->REQ-INST-3] auto-suspend: opt-in (no knob ⇒ never), threshold
    // counted from DORMANCY ONSET, fires only at/past the threshold, only
    // from dormant, and a dormant record missing its anchor fails warm.
    #[test]
    fn auto_suspend_counts_from_dormancy_onset_and_is_opt_in() {
        use RestEvent::AutoSuspendTick;
        use RestState::*;

        // Default OFF: no knob ⇒ no edge no matter how stale.
        assert_eq!(transition(Dormant, Some(0), AutoSuspendTick { now_ms: u64::MAX }, None), None);

        // Opt-in: dormant at t=1000, threshold 500.
        let knob = Some(500);
        assert_eq!(
            transition(Dormant, Some(1_000), AutoSuspendTick { now_ms: 1_499 }, knob),
            None,
            "inside the threshold — stays warm"
        );
        assert_eq!(
            transition(Dormant, Some(1_000), AutoSuspendTick { now_ms: 1_500 }, knob),
            Some(Suspended),
            "at the threshold — suspends"
        );

        // The anchor is dormancy onset, NOT last activity: a missing anchor
        // (torn legacy record) fails toward warm.
        assert_eq!(transition(Dormant, None, AutoSuspendTick { now_ms: u64::MAX }, knob), None);

        // The tick moves nothing else.
        assert_eq!(transition(Active, None, AutoSuspendTick { now_ms: u64::MAX }, knob), None);
        assert_eq!(transition(Suspended, None, AutoSuspendTick { now_ms: u64::MAX }, knob), None);
    }

    // [unit->REQ-INST-3] the knob chain: global default OFF, node enables,
    // endpoint overrides the node either way (0 = explicit endpoint OFF).
    #[test]
    fn knob_chain_resolves_global_node_endpoint() {
        // Global default OFF — nothing set anywhere.
        assert_eq!(effective_auto_suspend(None, None), None);
        // Node enables.
        assert_eq!(effective_auto_suspend(Some(60_000), None), Some(60_000));
        // Endpoint overrides the node's value.
        assert_eq!(effective_auto_suspend(Some(60_000), Some(5_000)), Some(5_000));
        // Endpoint 0 = explicit OFF even when the node enables.
        assert_eq!(effective_auto_suspend(Some(60_000), Some(0)), None);
        // Node 0 behaves as OFF too (symmetric).
        assert_eq!(effective_auto_suspend(Some(0), None), None);
        // Endpoint alone can enable.
        assert_eq!(effective_auto_suspend(None, Some(9)), Some(9));
    }

    // [unit->REQ-INST-3] durability round-trip: write_rest stamps the
    // dormancy anchor exactly on the dormant state (cleared otherwise),
    // read_rest reads it back, a recordless perch reads None (legacy
    // fallback), and a foreign future tag degrades to None instead of
    // wedging the reader.
    #[test]
    fn rest_record_round_trips_with_anchor_discipline() {
        let d = tempfile::tempdir().unwrap();
        write_info(d.path(), &InfoJson::new("ling", "t", 4242, "sid", "live_agent")).unwrap();

        assert_eq!(read_rest(d.path()), None, "pre-D9-2 record has no resting state");

        write_rest(d.path(), RestState::Dormant, 7_000).unwrap();
        let rec = read_rest(d.path()).expect("record present");
        assert_eq!(rec.state, RestState::Dormant);
        assert_eq!(rec.dormant_since_ms, Some(7_000), "anchor stamped at the edge");

        write_rest(d.path(), RestState::Active, 8_000).unwrap();
        let rec = read_rest(d.path()).expect("record present");
        assert_eq!(rec.state, RestState::Active);
        assert_eq!(rec.dormant_since_ms, None, "anchor cleared on leaving dormancy");

        write_rest(d.path(), RestState::Suspended, 9_000).unwrap();
        let rec = read_rest(d.path()).expect("record present");
        assert_eq!(rec.state, RestState::Suspended);
        assert_eq!(rec.dormant_since_ms, None, "suspended carries no dormancy anchor");

        // A foreign (newer-fleet) tag degrades to "no record", never an error.
        spt_store::info::set_rest_state(d.path(), "hibernating", None).unwrap();
        assert_eq!(read_rest(d.path()), None, "unknown tag reads as no resting record");

        // Tags round-trip through the parser.
        for s in [RestState::Active, RestState::Dormant, RestState::Suspended] {
            assert_eq!(RestState::from_tag(s.as_tag()), Some(s));
        }
        assert_eq!(RestState::from_tag(""), None);
    }
}


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

//! The resting-state model (D9-2, REQ-INST-3) — the explicit, daemon-owned
//! instance state machine behind dormant (warm) / suspended (cold).
//!
//! CONTEXT §dormant/suspended defines the model this module implements:
//!
//! - **active** = the most-recently-interacted instance for the ID *and* a
//!   driver attached. **No idle timer** — a session stays active while it
//!   holds both properties.
//! - **active → dormant**: the driver detaches, or attention shifts (another
//!   instance of the same id becomes the most-recently-interacted one).
//! - **dormant → suspended**: manual (`spt suspend` / shell
//!   `api owner-shutdown`), or opt-in auto-suspend after an
//!   `auto-suspend-after` threshold counted **from dormancy onset** — never
//!   from last activity (the [`InfoJson::dormant_since_ms`] anchor).
//! - **wake (→ active)**: re-activation in place — the state is already
//!   there; wake is an edge, not a spawn.
//!
//! The transition function is **pure** ([`transition`]): state + event +
//! resolved knob in, the new state out (`None` = no edge — the event does not
//! move this state). Durability is one field pair on the perch's `info.json`
//! ([`read_rest`] / [`write_rest`] over [`spt_store::info::set_rest_state`]);
//! effects (the transition echo commune, the wake resurface + freshness pull
//! — REQ-INST-4) hang off the *edges* and are wired by the transition host,
//! not here. Keeping the table pure is what makes the CONTEXT state diagram
//! a unit test instead of a prose promise.
//!
//! ## The auto-suspend knob chain (global → node → endpoint, default OFF)
//!
//! Auto-suspend is **opt-in, default OFF globally** (the compiled default:
//! no knob anywhere ⇒ `None` ⇒ never auto-suspends). A node may enable it
//! (`daemon.json` `auto_suspend_after_ms` — a constrained node defaults it
//! ON); an endpoint may override the node either way
//! (`info.json` `auto_suspend_after_ms`, with `0` = explicitly OFF even when
//! the node enables it). [`effective_auto_suspend`] resolves the chain.
//!
//! ## What "suspended" does NOT do here
//!
//! Suspension's session mechanics (closing the harness seat, resume-on-wake)
//! ride the existing live-agent machinery; this module owns the **state
//! model** those mechanics consult. The deferred-message instance-state gate
//! (REQ-INST-6 — deferred rows hold while the instance rests) is **M5**: the
//! state machine here is its precondition, the gate itself stays unbuilt
//! (`required_stages = []`, the documented seam).
// [impl->REQ-INST-3]

use std::path::Path;

use spt_store::info;

/// The resting states (CONTEXT §dormant/suspended): the registry's
/// `Status::Offline` is deliberately NOT here — offline means *node
/// unreachable*, a fact about the node observed by peers, never a state this
/// machine transitions an instance into.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RestState {
    /// Most-recently-interacted for the id + a driver attached.
    Active,
    /// Resting warm: the harness session stays running — instant
    /// re-activation. The default resting state.
    Dormant,
    /// Resting cold: the harness session is closed, resumed on wake.
    Suspended,
}

impl RestState {
    /// The durable `info.json` tag.
    pub fn as_tag(self) -> &'static str {
        match self {
            RestState::Active => "active",
            RestState::Dormant => "dormant",
            RestState::Suspended => "suspended",
        }
    }

    /// Parse a durable tag (`None` for an unknown/foreign value — a newer
    /// fleet's tag must degrade to "no resting record", never wedge a reader).
    pub fn from_tag(tag: &str) -> Option<RestState> {
        match tag {
            "active" => Some(RestState::Active),
            "dormant" => Some(RestState::Dormant),
            "suspended" => Some(RestState::Suspended),
            _ => None,
        }
    }
}

/// The events that can move an instance between resting states. Each names a
/// real observation point in the daemon (the production feeds), not a poll:
/// the model has **no idle timer** by design.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RestEvent {
    /// The driver detached (viewport ended, harness seat released).
    Detach,
    /// Another instance of the same id became the most-recently-interacted
    /// one (CONTEXT: driving `ling@laptop` makes `ling@desktop` dormant).
    AttentionShift,
    /// Manual suspend (`spt suspend` / shell `api owner-shutdown`).
    Suspend,
    /// The daemon's cadence check against the opt-in auto-suspend threshold.
    /// Carries the clock so the table stays pure.
    AutoSuspendTick { now_ms: u64 },
    /// A driver attached / `spt wake` / interaction — re-activate in place.
    Wake,
}

/// The pure transition table. `dormant_since_ms` is the dormancy-onset anchor
/// (present iff `state` is dormant); `auto_suspend_after_ms` is the RESOLVED
/// knob ([`effective_auto_suspend`] — `None` = OFF). Returns the new state on
/// a real edge, `None` when the event does not move this state (idempotent
/// re-delivery of any event is a no-op by construction — an event-feed replay
/// can never double-fire an edge's effects).
// [impl->REQ-INST-3]
pub fn transition(
    state: RestState,
    dormant_since_ms: Option<u64>,
    event: RestEvent,
    auto_suspend_after_ms: Option<u64>,
) -> Option<RestState> {
    match (state, event) {
        // active → dormant: the driver detaches or attention shifts.
        (RestState::Active, RestEvent::Detach) => Some(RestState::Dormant),
        (RestState::Active, RestEvent::AttentionShift) => Some(RestState::Dormant),
        // Manual suspend works from active too (the user said cold — the
        // active→suspended edge still fires the transition echo, KH 3.3).
        (RestState::Active, RestEvent::Suspend) => Some(RestState::Suspended),
        (RestState::Dormant, RestEvent::Suspend) => Some(RestState::Suspended),
        // Opt-in auto-suspend: threshold counted from DORMANCY ONSET. A
        // dormant record without its anchor (torn legacy state) never
        // auto-suspends — fail toward warm, the cheap mistake.
        (RestState::Dormant, RestEvent::AutoSuspendTick { now_ms }) => {
            let after = auto_suspend_after_ms.filter(|ms| *ms > 0)?;
            let since = dormant_since_ms?;
            (now_ms.saturating_sub(since) >= after).then_some(RestState::Suspended)
        }
        // wake = re-activation in place, from either resting state.
        (RestState::Dormant, RestEvent::Wake) => Some(RestState::Active),
        (RestState::Suspended, RestEvent::Wake) => Some(RestState::Active),
        // Everything else: no edge. Notably — Wake on an already-active
        // instance (just a recency stamp, not a transition), Detach /
        // AttentionShift on a resting one, Suspend on suspended (idempotent),
        // AutoSuspendTick anywhere but dormant.
        _ => None,
    }
}

/// Resolve the auto-suspend knob chain (REQ-INST-3): global default **OFF**
/// (`None`), node knob from `daemon.json`, endpoint override from the perch's
/// `info.json` — `Some(0)` at either level is an explicit OFF (an endpoint
/// can opt out of a node that defaults ON). Endpoint wins when present.
// [impl->REQ-INST-3]
pub fn effective_auto_suspend(node_ms: Option<u64>, endpoint_ms: Option<u64>) -> Option<u64> {
    endpoint_ms.or(node_ms).filter(|ms| *ms > 0)
}

/// One instance's durable resting record.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RestRecord {
    pub state: RestState,
    /// Dormancy-onset anchor (present iff `state` is dormant).
    pub dormant_since_ms: Option<u64>,
    /// The endpoint leg of the auto-suspend knob chain.
    pub auto_suspend_after_ms: Option<u64>,
}

/// Read a perch's resting record. `None` ⇒ no record (a pre-D9-2 / interim
/// perch, or an unreadable `info.json`) — callers fall back to the legacy
/// liveness-derived status, exactly what they did before this slice.
// [impl->REQ-INST-3]
pub fn read_rest(perch_path: &Path) -> Option<RestRecord> {
    let rec = info::read_info(perch_path)?;
    let state = RestState::from_tag(rec.rest_state.as_deref()?)?;
    Some(RestRecord {
        state,
        dormant_since_ms: rec.dormant_since_ms,
        auto_suspend_after_ms: rec.auto_suspend_after_ms,
    })
}

/// Persist a transition's landing state. Stamps the dormancy anchor when the
/// new state is dormant (the auto-suspend clock starts at the edge) and
/// clears it otherwise — one atomic write, so no reader ever sees a dormant
/// record without its anchor.
// [impl->REQ-INST-3]
pub fn write_rest(perch_path: &Path, state: RestState, now_ms: u64) -> std::io::Result<()> {
    let anchor = (state == RestState::Dormant).then_some(now_ms);
    info::set_rest_state(perch_path, state.as_tag(), anchor)
}

#[cfg(test)]
mod tests {
    use super::*;
    use spt_store::info::{write_info, InfoJson};

    // [unit->REQ-INST-3] THE transition table — the CONTEXT state diagram as
    // assertions: detach and attention-shift rest an active instance warm,
    // suspend rests it cold from either live state, wake re-activates in
    // place from either resting state, and everything else is a no-edge.
    #[test]
    fn transition_table_matches_the_context_model() {
        use RestEvent::*;
        use RestState::*;

        // active → dormant on detach / attention shift.
        assert_eq!(transition(Active, None, Detach, None), Some(Dormant));
        assert_eq!(transition(Active, None, AttentionShift, None), Some(Dormant));
        // manual suspend from active AND from dormant.
        assert_eq!(transition(Active, None, Suspend, None), Some(Suspended));
        assert_eq!(transition(Dormant, Some(5), Suspend, None), Some(Suspended));
        // wake re-activates from either resting state.
        assert_eq!(transition(Dormant, Some(5), Wake, None), Some(Active));
        assert_eq!(transition(Suspended, None, Wake, None), Some(Active));

        // No-edges: events are idempotent against a state they don't move —
        // a replayed feed can never double-fire an edge.
        assert_eq!(transition(Active, None, Wake, None), None, "already active");
        assert_eq!(transition(Dormant, Some(5), Detach, None), None);
        assert_eq!(transition(Dormant, Some(5), AttentionShift, None), None);
        assert_eq!(transition(Suspended, None, Suspend, None), None, "suspend is idempotent");
        assert_eq!(transition(Suspended, None, Detach, None), None);
        assert_eq!(transition(Suspended, None, AttentionShift, None), None);
    }

    // [unit->REQ-INST-3] auto-suspend: opt-in (no knob ⇒ never), threshold
    // counted from DORMANCY ONSET, fires only at/past the threshold, only
    // from dormant, and a dormant record missing its anchor fails warm.
    #[test]
    fn auto_suspend_counts_from_dormancy_onset_and_is_opt_in() {
        use RestEvent::AutoSuspendTick;
        use RestState::*;

        // Default OFF: no knob ⇒ no edge no matter how stale.
        assert_eq!(transition(Dormant, Some(0), AutoSuspendTick { now_ms: u64::MAX }, None), None);

        // Opt-in: dormant at t=1000, threshold 500.
        let knob = Some(500);
        assert_eq!(
            transition(Dormant, Some(1_000), AutoSuspendTick { now_ms: 1_499 }, knob),
            None,
            "inside the threshold — stays warm"
        );
        assert_eq!(
            transition(Dormant, Some(1_000), AutoSuspendTick { now_ms: 1_500 }, knob),
            Some(Suspended),
            "at the threshold — suspends"
        );

        // The anchor is dormancy onset, NOT last activity: a missing anchor
        // (torn legacy record) fails toward warm.
        assert_eq!(transition(Dormant, None, AutoSuspendTick { now_ms: u64::MAX }, knob), None);

        // The tick moves nothing else.
        assert_eq!(transition(Active, None, AutoSuspendTick { now_ms: u64::MAX }, knob), None);
        assert_eq!(transition(Suspended, None, AutoSuspendTick { now_ms: u64::MAX }, knob), None);
    }

    // [unit->REQ-INST-3] the knob chain: global default OFF, node enables,
    // endpoint overrides the node either way (0 = explicit endpoint OFF).
    #[test]
    fn knob_chain_resolves_global_node_endpoint() {
        // Global default OFF — nothing set anywhere.
        assert_eq!(effective_auto_suspend(None, None), None);
        // Node enables.
        assert_eq!(effective_auto_suspend(Some(60_000), None), Some(60_000));
        // Endpoint overrides the node's value.
        assert_eq!(effective_auto_suspend(Some(60_000), Some(5_000)), Some(5_000));
        // Endpoint 0 = explicit OFF even when the node enables.
        assert_eq!(effective_auto_suspend(Some(60_000), Some(0)), None);
        // Node 0 behaves as OFF too (symmetric).
        assert_eq!(effective_auto_suspend(Some(0), None), None);
        // Endpoint alone can enable.
        assert_eq!(effective_auto_suspend(None, Some(9)), Some(9));
    }

    // [unit->REQ-INST-3] durability round-trip: write_rest stamps the
    // dormancy anchor exactly on the dormant state (cleared otherwise),
    // read_rest reads it back, a recordless perch reads None (legacy
    // fallback), and a foreign future tag degrades to None instead of
    // wedging the reader.
    #[test]
    fn rest_record_round_trips_with_anchor_discipline() {
        let d = tempfile::tempdir().unwrap();
        write_info(d.path(), &InfoJson::new("ling", "t", 4242, "sid", "live_agent")).unwrap();

        assert_eq!(read_rest(d.path()), None, "pre-D9-2 record has no resting state");

        write_rest(d.path(), RestState::Dormant, 7_000).unwrap();
        let rec = read_rest(d.path()).expect("record present");
        assert_eq!(rec.state, RestState::Dormant);
        assert_eq!(rec.dormant_since_ms, Some(7_000), "anchor stamped at the edge");

        write_rest(d.path(), RestState::Active, 8_000).unwrap();
        let rec = read_rest(d.path()).expect("record present");
        assert_eq!(rec.state, RestState::Active);
        assert_eq!(rec.dormant_since_ms, None, "anchor cleared on leaving dormancy");

        write_rest(d.path(), RestState::Suspended, 9_000).unwrap();
        let rec = read_rest(d.path()).expect("record present");
        assert_eq!(rec.state, RestState::Suspended);
        assert_eq!(rec.dormant_since_ms, None, "suspended carries no dormancy anchor");

        // A foreign (newer-fleet) tag degrades to "no record", never an error.
        spt_store::info::set_rest_state(d.path(), "hibernating", None).unwrap();
        assert_eq!(read_rest(d.path()), None, "unknown tag reads as no resting record");

        // Tags round-trip through the parser.
        for s in [RestState::Active, RestState::Dormant, RestState::Suspended] {
            assert_eq!(RestState::from_tag(s.as_tag()), Some(s));
        }
        assert_eq!(RestState::from_tag(""), None);
    }
}


**4. Config knob:**


Edit

    #[serde(default, skip_serializing_if = "Option::is_none")]
    update_check_period_ms: Option<u64>,
}

    #[serde(default, skip_serializing_if = "Option::is_none")]
    update_check_period_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    auto_suspend_after_ms: Option<u64>,
}


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

    #[serde(default, skip_serializing_if = "Option::is_none")]
    update_check_period_ms: Option<u64>,
}

    #[serde(default, skip_serializing_if = "Option::is_none")]
    update_check_period_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    auto_suspend_after_ms: Option<u64>,
}

+    #[serde(default, skip_serializing_if = "Option::is_none")]

+    auto_suspend_after_ms: Option<u64>,

/// Resolved daemon configuration. The brain's scheduled loops read their cadence


    /// Update check cadence (D9-1; the pull rides the same trusted conns).
    pub update_check_period: Duration,
}

    /// Update check cadence (D9-1; the pull rides the same trusted conns).
    pub update_check_period: Duration,
    /// The NODE leg of the auto-suspend knob chain (D9-2, REQ-INST-3):
    /// dormant → suspended after this many ms counted from dormancy onset.
    /// **`None` = the global default, OFF** — auto-suspend is opt-in
    /// (CONTEXT §dormant/suspended); a constrained node sets it here, and a
    /// per-endpoint `info.json` override wins over this value
    /// ([`crate::resting::effective_auto_suspend`]).
    pub auto_suspend_after_ms: Option<u64>,
}


    /// Update check cadence (D9-1; the pull rides the same trusted conns).
    pub update_check_period: Duration,
}

    /// Update check cadence (D9-1; the pull rides the same trusted conns).
    pub update_check_period: Duration,
    /// The NODE leg of the auto-suspend knob chain (D9-2, REQ-INST-3):
    /// dormant → suspended after this many ms counted from dormancy onset.
    /// **`None` = the global default, OFF** — auto-suspend is opt-in
    /// (CONTEXT §dormant/suspended); a constrained node sets it here, and a
    /// per-endpoint `info.json` override wins over this value
    /// ([`crate::resting::effective_auto_suspend`]).
    pub auto_suspend_after_ms: Option<u64>,
}

+    /// The NODE leg of the auto-suspend knob chain (D9-2, REQ-INST-3):

+    /// dormant → suspended after this many ms counted from dormancy onset.

+    /// **`None` = the global default, OFF** — auto-suspend is opt-in

+    /// (CONTEXT §dormant/suspended); a constrained node sets it here, and a

+    /// per-endpoint `info.json` override wins over this value

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

+    pub auto_suspend_after_ms: Option<u64>,


            sync_pull_period: DEFAULT_SYNC_PULL_PERIOD,
            update_check_period: DEFAULT_UPDATE_CHECK_PERIOD,
        }
    }
}

            sync_pull_period: DEFAULT_SYNC_PULL_PERIOD,
            update_check_period: DEFAULT_UPDATE_CHECK_PERIOD,
            auto_suspend_after_ms: None, // opt-in, default OFF (REQ-INST-3)
        }
    }
}


            sync_pull_period: DEFAULT_SYNC_PULL_PERIOD,
            update_check_period: DEFAULT_UPDATE_CHECK_PERIOD,
        }
    }
}

            sync_pull_period: DEFAULT_SYNC_PULL_PERIOD,
            update_check_period: DEFAULT_UPDATE_CHECK_PERIOD,
            auto_suspend_after_ms: None, // opt-in, default OFF (REQ-INST-3)
        }
    }
}

+            auto_suspend_after_ms: None, // opt-in, default OFF (REQ-INST-3)


            update_check_period: file
                .update_check_period_ms
                .map(Duration::from_millis)
                .unwrap_or(d.update_check_period),
        }
    }

            update_check_period: file
                .update_check_period_ms
                .map(Duration::from_millis)
                .unwrap_or(d.update_check_period),
            auto_suspend_after_ms: file.auto_suspend_after_ms,
        }
    }


            update_check_period: file
                .update_check_period_ms
                .map(Duration::from_millis)
                .unwrap_or(d.update_check_period),
        }
    }

            update_check_period: file
                .update_check_period_ms
                .map(Duration::from_millis)
                .unwrap_or(d.update_check_period),
            auto_suspend_after_ms: file.auto_suspend_after_ms,
        }
    }

+            auto_suspend_after_ms: file.auto_suspend_after_ms,


            sync_pull_period_ms: Some(self.sync_pull_period.as_millis() as u64),
            update_check_period_ms: Some(self.update_check_period.as_millis() as u64),
        };

            sync_pull_period_ms: Some(self.sync_pull_period.as_millis() as u64),
            update_check_period_ms: Some(self.update_check_period.as_millis() as u64),
            auto_suspend_after_ms: self.auto_suspend_after_ms,
        };


            sync_pull_period_ms: Some(self.sync_pull_period.as_millis() as u64),
            update_check_period_ms: Some(self.update_check_period.as_millis() as u64),
        };

            sync_pull_period_ms: Some(self.sync_pull_period.as_millis() as u64),
            update_check_period_ms: Some(self.update_check_period.as_millis() as u64),
            auto_suspend_after_ms: self.auto_suspend_after_ms,
        };

notif_pump_period_ms: Some(self.notif_pump_period.as_millis() as u64),

+            auto_suspend_after_ms: self.auto_suspend_after_ms,

let json = serde_json::to_string_pretty(&file)


Fix config test literal + add knob test:


            sync_pull_period: Duration::from_millis(13_000),
            update_check_period: Duration::from_millis(14_000),
        };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);
    }

            sync_pull_period: Duration::from_millis(13_000),
            update_check_period: Duration::from_millis(14_000),
            auto_suspend_after_ms: Some(15_000),
        };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);
    }

    // [unit->REQ-INST-3] the node auto-suspend knob is opt-in: absent ⇒ OFF
    // (the global default), present ⇒ the node leg of the knob chain.
    #[test]
    fn auto_suspend_knob_is_opt_in_default_off() {
        assert_eq!(DaemonConfig::default().auto_suspend_after_ms, None, "default OFF");

        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("daemon.json");
        std::fs::write(&path, r#"{"auto_suspend_after_ms": 300000}"#).unwrap();
        assert_eq!(DaemonConfig::load_from(&path).auto_suspend_after_ms, Some(300_000));
        // An absent file keeps the OFF default.
        assert_eq!(
            DaemonConfig::load_from(&dir.path().join("nope.json")).auto_suspend_after_ms,
            None
        );
    }


            sync_pull_period: Duration::from_millis(13_000),
            update_check_period: Duration::from_millis(14_000),
        };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);
    }

            sync_pull_period: Duration::from_millis(13_000),
            update_check_period: Duration::from_millis(14_000),
            auto_suspend_after_ms: Some(15_000),
        };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);
    }

    // [unit->REQ-INST-3] the node auto-suspend knob is opt-in: absent ⇒ OFF
    // (the global default), present ⇒ the node leg of the knob chain.
    #[test]
    fn auto_suspend_knob_is_opt_in_default_off() {
        assert_eq!(DaemonConfig::default().auto_suspend_after_ms, None, "default OFF");

        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("daemon.json");
        std::fs::write(&path, r#"{"auto_suspend_after_ms": 300000}"#).unwrap();
        assert_eq!(DaemonConfig::load_from(&path).auto_suspend_after_ms, Some(300_000));
        // An absent file keeps the OFF default.
        assert_eq!(
            DaemonConfig::load_from(&dir.path().join("nope.json")).auto_suspend_after_ms,
            None
        );
    }

+            auto_suspend_after_ms: Some(15_000),

+    // [unit->REQ-INST-3] the node auto-suspend knob is opt-in: absent ⇒ OFF

+    // (the global default), present ⇒ the node leg of the knob chain.

+    fn auto_suspend_knob_is_opt_in_default_off() {

+        assert_eq!(DaemonConfig::default().auto_suspend_after_ms, None, "default OFF");

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

+        let path = dir.path().join("daemon.json");

+        std::fs::write(&path, r#"{"auto_suspend_after_ms": 300000}"#).unwrap();

+        assert_eq!(DaemonConfig::load_from(&path).auto_suspend_after_ms, Some(300_000));

+        // An absent file keeps the OFF default.

+            DaemonConfig::load_from(&dir.path().join("nope.json")).auto_suspend_after_ms,

#[test] // [unit->REQ-UPD-4] full-auto update is OFF by default and config-driven.

assert!(!DaemonConfig::default().full_auto_update, "gated by default (consent required)");


**5. advertise_local follows the resting record:**


        for id in perch::list_self_perch_ids(owlery) {
            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);
            let status =
                if is_perch_alive(&perch_path) { Status::Active } else { Status::Dormant };

        for id in perch::list_self_perch_ids(owlery) {
            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);
            // Advertisement follows the resting-state machine (D9-2,
            // REQ-INST-3): a live perch with a resting record advertises its
            // machine state (active / dormant / suspended); a recordless
            // (pre-D9-2 / interim) or dead perch keeps the legacy
            // liveness-derived mapping. Every advertisement stamps a fresh
            // epoch below, so a transition reaches peers as an epoch-bumped
            // update — the lease orders it (KH 4.8).
            // [impl->REQ-INST-3]
            let status = if is_perch_alive(&perch_path) {
                match crate::resting::read_rest(&perch_path).map(|r| r.state) {
                    Some(crate::resting::RestState::Active) | None => Status::Active,
                    Some(crate::resting::RestState::Dormant) => Status::Dormant,
                    Some(crate::resting::RestState::Suspended) => Status::Suspended,
                }
            } else {
                Status::Dormant
            };


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

        for id in perch::list_self_perch_ids(owlery) {
            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);
            let status =
                if is_perch_alive(&perch_path) { Status::Active } else { Status::Dormant };

        for id in perch::list_self_perch_ids(owlery) {
            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);
            // Advertisement follows the resting-state machine (D9-2,
            // REQ-INST-3): a live perch with a resting record advertises its
            // machine state (active / dormant / suspended); a recordless
            // (pre-D9-2 / interim) or dead perch keeps the legacy
            // liveness-derived mapping. Every advertisement stamps a fresh
            // epoch below, so a transition reaches peers as an epoch-bumped
            // update — the lease orders it (KH 4.8).
            // [impl->REQ-INST-3]
            let status = if is_perch_alive(&perch_path) {
                match crate::resting::read_rest(&perch_path).map(|r| r.state) {
                    Some(crate::resting::RestState::Active) | None => Status::Active,
                    Some(crate::resting::RestState::Dormant) => Status::Dormant,
                    Some(crate::resting::RestState::Suspended) => Status::Suspended,
                }
            } else {
                Status::Dormant
            };

-                if is_perch_alive(&perch_path) { Status::Active } else { Status::Dormant };

+            // Advertisement follows the resting-state machine (D9-2,

+            // REQ-INST-3): a live perch with a resting record advertises its

+            // machine state (active / dormant / suspended); a recordless

+            // (pre-D9-2 / interim) or dead perch keeps the legacy

+            // liveness-derived mapping. Every advertisement stamps a fresh

+            // epoch below, so a transition reaches peers as an epoch-bumped

+            // update — the lease orders it (KH 4.8).

+            let status = if is_perch_alive(&perch_path) {

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

+                    Some(crate::resting::RestState::Active) | None => Status::Active,

+                    Some(crate::resting::RestState::Dormant) => Status::Dormant,

+                    Some(crate::resting::RestState::Suspended) => Status::Suspended,

let Ok(ep) = epoch.next_epoch() else { continue };


Add "status follows transitions" test to registryhost:


    // [unit->REQ-INST-7] advertise_local scans the owlery, stamps fresh
    // epochs, respects visibility, and returns only this node's rows.

    // [unit->REQ-INST-3] registry advertisement FOLLOWS the resting-state
    // machine: a live perch advertises its machine state (active → dormant →
    // suspended), each transition reaching the registry as an epoch-bumped
    // update (the lease orders them); a recordless perch keeps the legacy
    // liveness-derived Active.
    #[test]
    fn advertisement_follows_resting_transitions_epoch_bumped() {
        with_home(|home| {
            let owlery = home.join("owlery");
            let p = perch::resolve_perch_path_in(&owlery, "ling", ParentHint::Infer);
            std::fs::create_dir_all(&p).unwrap();
            let rec = InfoJson::new("ling", "now", std::process::id(), "s", "live_agent");
            info::write_info(&p, &rec).unwrap();
            let mut subnets = SubnetStore::load();
            subnets.create_subnet("home").unwrap();
            subnets.save().unwrap();

            let h = host(home);

            // No resting record yet (pre-D9-2 parity): live ⇒ Active.
            h.advertise_local(&owlery);
            let row = h.rows("home", "ling")[0].clone();
            assert_eq!(row.status, Status::Active, "recordless live perch reads Active");

            // active → dormant: the next advertisement carries Dormant at a
            // strictly greater epoch.
            crate::resting::write_rest(&p, crate::resting::RestState::Dormant, 1_000).unwrap();
            h.advertise_local(&owlery);
            let dormant = h.rows("home", "ling")[0].clone();
            assert_eq!(dormant.status, Status::Dormant, "advertisement follows the machine");
            assert!(dormant.epoch > row.epoch, "transition is epoch-bumped (the lease orders it)");

            // dormant → suspended.
            crate::resting::write_rest(&p, crate::resting::RestState::Suspended, 2_000).unwrap();
            h.advertise_local(&owlery);
            let suspended = h.rows("home", "ling")[0].clone();
            assert_eq!(suspended.status, Status::Suspended);
            assert!(suspended.epoch > dormant.epoch);

            // wake: suspended → active again.
            crate::resting::write_rest(&p, crate::resting::RestState::Active, 3_000).unwrap();
            h.advertise_local(&owlery);
            assert_eq!(h.rows("home", "ling")[0].status, Status::Active, "wake re-advertises Active");
        });
    }

    // [unit->REQ-INST-7] advertise_local scans the owlery, stamps fresh
    // epochs, respects visibility, and returns only this node's rows.


    // [unit->REQ-INST-3] registry advertisement FOLLOWS the resting-state
    // machine: a live perch advertises its machine state (active → dormant →
    // suspended), each transition reaching the registry as an epoch-bumped
    // update (the lease orders them); a recordless perch keeps the legacy
    // liveness-derived Active.
    #[test]
    fn advertisement_follows_resting_transitions_epoch_bumped() {
        with_home(|home| {
            let owlery = home.join("owlery");
            let p = perch::resolve_perch_path_in(&owlery, "ling", ParentHint::Infer);
            std::fs::create_dir_all(&p).unwrap();
            let rec = InfoJson::new("ling", "now", std::process::id(), "s", "live_agent");
            info::write_info(&p, &rec).unwrap();
            let mut subnets = SubnetStore::load();
            subnets.create_subnet("home").unwrap();
            subnets.save().unwrap();

            let h = host(home);

            // No resting record yet (pre-D9-2 parity): live ⇒ Active.
            h.advertise_local(&owlery);
            let row = h.rows("home", "ling")[0].clone();
            assert_eq!(row.status, Status::Active, "recordless live perch reads Active");

            // active → dormant: the next advertisement carries Dormant at a
            // strictly greater epoch.
            crate::resting::write_rest(&p, crate::resting::RestState::Dormant, 1_000).unwrap();
            h.advertise_local(&owlery);
            let dormant = h.rows("home", "ling")[0].clone();
            assert_eq!(dormant.status, Status::Dormant, "advertisement follows the machine");
            assert!(dormant.epoch > row.epoch, "transition is epoch-bumped (the lease orders it)");

            // dormant → suspended.
            crate::resting::write_rest(&p, crate::resting::RestState::Suspended, 2_000).unwrap();
            h.advertise_local(&owlery);
            let suspended = h.rows("home", "ling")[0].clone();
            assert_eq!(suspended.status, Status::Suspended);
            assert!(suspended.epoch > dormant.epoch);

            // wake: suspended → active again.
            crate::resting::write_rest(&p, crate::resting::RestState::Active, 3_000).unwrap();
            h.advertise_local(&owlery);
            assert_eq!(h.rows("home", "ling")[0].status, Status::Active, "wake re-advertises Active");
        });
    }

    // [unit->REQ-INST-7] advertise_local scans the owlery, stamps fresh
    // epochs, respects visibility, and returns only this node's rows.

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

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

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

+    // update (the lease orders them); a recordless perch keeps the legacy

+    fn advertisement_follows_resting_transitions_epoch_bumped() {

+            let owlery = home.join("owlery");

+            let p = perch::resolve_perch_path_in(&owlery, "ling", ParentHint::Infer);

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

+            let rec = InfoJson::new("ling", "now", std::process::id(), "s", "live_agent");

+            info::write_info(&p, &rec).unwrap();

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

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

+            // No resting record yet (pre-D9-2 parity): live ⇒ Active.

+            h.advertise_local(&owlery);

+            let row = h.rows("home", "ling")[0].clone();

+            assert_eq!(row.status, Status::Active, "recordless live perch reads Active");

+            // active → dormant: the next advertisement carries Dormant at a

+            crate::resting::write_rest(&p, crate::resting::RestState::Dormant, 1_000).unwrap();

+            let dormant = h.rows("home", "ling")[0].clone();

+            assert_eq!(dormant.status, Status::Dormant, "advertisement follows the machine");

+            assert!(dormant.epoch > row.epoch, "transition is epoch-bumped (the lease orders it)");

+            crate::resting::write_rest(&p, crate::resting::RestState::Suspended, 2_000).unwrap();

+            let suspended = h.rows("home", "ling")[0].clone();

+            assert_eq!(suspended.status, Status::Suspended);

+            assert!(suspended.epoch > dormant.epoch);

+            // wake: suspended → active again.

+            crate::resting::write_rest(&p, crate::resting::RestState::Active, 3_000).unwrap();

+            assert_eq!(h.rows("home", "ling")[0].status, Status::Active, "wake re-advertises Active");


**6. lib.rs wiring:**


pub mod registryhost;
pub mod relay;
pub mod resting;