Skip to main content

microscope_memory/
consciousness_seqlock.rs

1//! Lock-free consciousness snapshot, protected by a **seqlock** protocol.
2//!
3//! # What is this?
4//!
5//! A snapshot of the consciousness stream's state. The stream's background
6//! cycle publishes a `SharedSnapshot` once per tick; any number of readers in
7//! the **same process** can read it **without taking a Mutex**, **without
8//! copying**, **without serializing**.
9//!
10//! # Why is this useful?
11//!
12//! The hot read path (`ConsciousnessStream::format`) historically needed a
13//! `Mutex` lock plus 28k-element sum = ~25 µs. With pre-computed aggregates
14//! plus seqlock reads, the cost is bounded by one atomic load plus one
15//! fixed-size struct copy.
16//!
17//! # Why "impossible but possible"?
18//!
19//! Reading a multi-field structure while another thread writes it
20//! classically requires either a lock (defeats concurrency) or a copy
21//! (defeats speed). The seqlock sidesteps both:
22//!
23//!   - Writer increments a sequence counter (odd during write)
24//!   - Writer writes the data
25//!   - Writer increments again (even after write)
26//!   - Reader checks sequence (must be even), reads data, rechecks
27//!   - If sequence changed, retry
28//!
29//! For 28k-element data, retries are extremely rare because the writer
30//! holds the lock for microseconds and the reader holds for nanoseconds.
31//!
32//! # Cross-process (mmap) layer
33//!
34//! The in-process path (`SharedSnapshot` behind `Arc`) is used by
35//! `ConsciousnessStream`. For cross-process federation, the file-backed mmap
36//! layer below (`MappedSnapshot` / `MappedSnapshotWriter`) maps the seqlock
37//! header + `SnapshotData` block — the `#[repr(C)]` prefix that is
38//! byte-identical to `SharedSnapshot`'s layout — so two processes looking at
39//! the same file see the same snapshot with the same seqlock protocol. The
40//! heap-backed fast-path fields (`RwLock<String>`, hot atomics) stay
41//! in-process only. The stream's background cycle publishes to it once per
42//! tick (see `consciousness_stream`), so another process can map the file and
43//! read the live snapshot.
44
45use std::cell::UnsafeCell;
46use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
47use std::sync::RwLock;
48
49/// Magic number identifying a valid snapshot ("CONS" in little-endian).
50pub const SNAPSHOT_MAGIC: u32 = 0x534E_4F43;
51/// Snapshot format version. Bumped on incompatible layout changes.
52pub const SNAPSHOT_VERSION: u32 = 1;
53/// Maximum retries on a torn read before giving up.
54pub const SNAPSHOT_MAX_RETRIES: u32 = 8;
55/// File name (inside the configured output dir) for the shared snapshot.
56pub const SNAPSHOT_FILE_NAME: &str = "consciousness_snapshot.bin";
57
58/// 96-byte snapshot of the consciousness stream. All fields are
59/// pre-computed by the writer; readers do no derivation.
60/// `#[repr(C)]` keeps the seqlock header and data block layout stable
61/// (relevant if a future file-mmap layer is ever built).
62/// The data fields live inside `UnsafeCell` so the writer can mutate them
63/// through `&self` (required for the seqlock protocol). The seqlock
64/// guarantees readers never see a torn write.
65#[repr(C)]
66pub struct SharedSnapshot {
67    /// Seqlock counter. Even = stable, odd = write in progress.
68    pub sequence: AtomicU64,
69    /// `SNAPSHOT_MAGIC`. Lets readers detect a fresh/uninitialized snapshot.
70    pub magic: u32,
71    /// `SNAPSHOT_VERSION`. Bumped on layout change.
72    pub version: u32,
73    /// Reserved padding to align the UnsafeCell to 8 bytes.
74    _pad: [u32; 2],
75
76    /// Mutable data block. UnsafeCell is the standard Rust idiom for
77    /// interior mutability behind `&self`. Layout is stable thanks to
78    /// `#[repr(C)]` and explicit field types.
79    data: UnsafeCell<SnapshotData>,
80
81    // ─── Fast-path fields (heap-backed, in-process only) ───────────
82    /// Pre-formatted consciousness string. Updated by the background cycle.
83    /// Readers clone this in O(1) without any format!() calls.
84    cached_format: RwLock<String>,
85    /// Lock-free cycle counter. Readers can check freshness without seqlock.
86    pub hot_cycle: AtomicU64,
87    /// Lock-free surprise level (f32 stored as u32 bits).
88    pub hot_surprise_bits: AtomicU32,
89    /// Lock-free curiosity level (f32 stored as u32 bits).
90    pub hot_curiosity_bits: AtomicU32,
91    /// Lock-free predicted query hash.
92    pub hot_predicted_hash: AtomicU64,
93}
94
95/// Inner data block. Plain scalars, no atomics — seqlock protects them.
96#[repr(C)]
97#[derive(Clone, Copy, Debug)]
98pub struct SnapshotData {
99    pub cycle: u64,
100    pub last_query_ms: u64,
101    pub activations_count: u32,
102    pub activations_total_energy: f64,
103    pub attention_layers: u32,
104    pub resonance_cells: u32,
105    pub patterns_crystallized: u32,
106    pub predictions_count: u32,
107    pub predictions_hit_rate: f32,
108    pub archetypes_count: u32,
109    pub mirror_echoes: u32,
110    pub predicted_query_hash: u64,
111    pub predicted_confidence: f32,
112    pub surprise_level: f32,
113    pub curiosity_level: f32,
114    pub emo_intensity: f32,
115    pub emo_dominant_idx: i32,
116    pub emo_dominant_val: f32,
117    _pad: [u8; 8],
118}
119
120impl SnapshotData {
121    pub const fn zeroed() -> Self {
122        Self {
123            cycle: 0,
124            last_query_ms: 0,
125            activations_count: 0,
126            activations_total_energy: 0.0,
127            attention_layers: 0,
128            resonance_cells: 0,
129            patterns_crystallized: 0,
130            predictions_count: 0,
131            predictions_hit_rate: 0.0,
132            archetypes_count: 0,
133            mirror_echoes: 0,
134            predicted_query_hash: 0,
135            predicted_confidence: 0.0,
136            surprise_level: 0.0,
137            curiosity_level: 0.0,
138            emo_intensity: 0.0,
139            emo_dominant_idx: -1,
140            emo_dominant_val: 0.0,
141            _pad: [0; 8],
142        }
143    }
144}
145
146// SAFETY: `SharedSnapshot` is a seqlock. The seqlock protocol guarantees
147// that any data read happens either before or after any data write —
148// never during. Multiple readers can hold `&SharedSnapshot` concurrently
149// because `read()` does not mutate. The single writer must hold the
150// seqlock (via `begin_write`/`end_write`) before calling `data_mut`.
151unsafe impl Sync for SharedSnapshot {}
152unsafe impl Send for SharedSnapshot {}
153
154impl std::fmt::Debug for SharedSnapshot {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        f.debug_struct("SharedSnapshot")
157            .field("sequence", &self.sequence.load(Ordering::Relaxed))
158            .field("magic", &self.magic)
159            .field("version", &self.version)
160            .finish()
161    }
162}
163
164impl SharedSnapshot {
165    /// Build a zeroed snapshot, ready to be written.
166    pub fn new_zeroed() -> Self {
167        Self {
168            sequence: AtomicU64::new(0),
169            magic: SNAPSHOT_MAGIC,
170            version: SNAPSHOT_VERSION,
171            _pad: [0; 2],
172            data: UnsafeCell::new(SnapshotData::zeroed()),
173            cached_format: RwLock::new(String::new()),
174            hot_cycle: AtomicU64::new(0),
175            hot_surprise_bits: AtomicU32::new(0),
176            hot_curiosity_bits: AtomicU32::new(0),
177            hot_predicted_hash: AtomicU64::new(0),
178        }
179    }
180
181    /// Begin a write. Increments sequence to an odd value, returns the
182    /// value the writer must restore after the write to mark completion.
183    /// Caller MUST call `end_write` with the returned value, even on panic.
184    pub fn begin_write(&self) -> u64 {
185        let s = self.sequence.fetch_add(1, Ordering::AcqRel);
186        std::sync::atomic::fence(Ordering::Release);
187        s + 1
188    }
189
190    /// End a write. Stores an even sequence value, signaling readers that
191    /// the data is consistent.
192    pub fn end_write(&self, expected: u64) {
193        std::sync::atomic::fence(Ordering::Release);
194        self.sequence.store(expected + 1, Ordering::Release);
195    }
196
197    /// Get an exclusive `&mut` reference to the data block. The seqlock
198    /// protocol guarantees no reader can be accessing the data while this
199    /// is held (the sequence is odd, so readers retry).
200    ///
201    /// # Safety
202    ///
203    /// Caller must hold the seqlock by having called `begin_write` and
204    /// not yet called `end_write`. Only one writer at a time.
205    #[allow(clippy::mut_from_ref)]
206    pub unsafe fn data_mut(&self) -> &mut SnapshotData {
207        &mut *self.data.get()
208    }
209
210    // ─── Fast-path read methods ────────────────────────────
211
212    /// Read the pre-formatted consciousness string. O(1) clone, no format!().
213    /// This is the fastest path for the MCP tool: ~50-100ns per call.
214    pub fn read_cached_format(&self) -> String {
215        match self.cached_format.read() {
216            Ok(guard) => guard.clone(),
217            Err(_) => "🧠 Consciousness Stream — (cache poisoned)".to_string(),
218        }
219    }
220
221    /// Read hot fields atomically without seqlock. ~5-20ns.
222    /// Returns (cycle, surprise, curiosity, predicted_hash).
223    pub fn read_hot_fields(&self) -> (u64, f32, f32, u64) {
224        let cycle = self.hot_cycle.load(Ordering::Relaxed);
225        let surprise = f32::from_bits(self.hot_surprise_bits.load(Ordering::Relaxed));
226        let curiosity = f32::from_bits(self.hot_curiosity_bits.load(Ordering::Relaxed));
227        let hash = self.hot_predicted_hash.load(Ordering::Relaxed);
228        (cycle, surprise, curiosity, hash)
229    }
230
231    /// Check if the snapshot is fresh (hot_cycle matches or exceeds expected).
232    pub fn is_fresh(&self, expected_cycle: u64) -> bool {
233        self.hot_cycle.load(Ordering::Relaxed) >= expected_cycle
234    }
235
236    /// Update cached format string. Called by the background cycle.
237    pub fn set_cached_format(&self, s: String) {
238        if let Ok(mut guard) = self.cached_format.write() {
239            *guard = s;
240        }
241    }
242
243    /// Update hot atomic fields. Called by the background cycle.
244    pub fn set_hot_fields(&self, cycle: u64, surprise: f32, curiosity: f32, predicted_hash: u64) {
245        self.hot_cycle.store(cycle, Ordering::Relaxed);
246        self.hot_surprise_bits
247            .store(surprise.to_bits(), Ordering::Relaxed);
248        self.hot_curiosity_bits
249            .store(curiosity.to_bits(), Ordering::Relaxed);
250        self.hot_predicted_hash
251            .store(predicted_hash, Ordering::Relaxed);
252    }
253
254    /// Read the snapshot. Returns `None` after `SNAPSHOT_MAX_RETRIES` torn reads.
255    /// The returned `SnapshotData` is a copy — no lock required to use it.
256    pub fn read(&self) -> Option<SnapshotData> {
257        for _ in 0..SNAPSHOT_MAX_RETRIES {
258            let s1 = self.sequence.load(Ordering::Acquire);
259            if s1 & 1 == 1 {
260                std::hint::spin_loop();
261                continue;
262            }
263            std::sync::atomic::fence(Ordering::Acquire);
264            // SAFETY: sequence is even and stable (s1 & 1 == 0), the writer
265            // is not currently mutating data. We do a single read of the
266            // whole data block; torn reads are detected by the post-read
267            // sequence check.
268            let data = unsafe { *self.data.get() };
269            std::sync::atomic::fence(Ordering::Acquire);
270            let s2 = self.sequence.load(Ordering::Acquire);
271            if s1 == s2 && self.magic == SNAPSHOT_MAGIC {
272                return Some(data);
273            }
274        }
275        None
276    }
277}
278
279/// File-backed, cross-process seqlock snapshot.
280///
281/// Only the seqlock header and the `SnapshotData` block are shared; the layout
282/// is the `#[repr(C)]` prefix of `SharedSnapshot`, so the two are
283/// byte-compatible. The seqlock protocol (odd/even sequence, retry on torn
284/// reads) is identical across processes — the OS guarantees coherence of
285/// `MAP_SHARED` pages.
286#[cfg(not(target_arch = "wasm32"))]
287#[repr(C)]
288pub struct MmapSnapshot {
289    sequence: AtomicU64,
290    magic: u32,
291    version: u32,
292    _pad: [u32; 2],
293    data: UnsafeCell<SnapshotData>,
294}
295
296// SAFETY: `MmapSnapshot` is a seqlock. The protocol guarantees readers never
297// observe a torn write, and the single writer must hold the seqlock before
298// touching `data`. This mirrors the `SharedSnapshot` safety argument.
299#[cfg(not(target_arch = "wasm32"))]
300unsafe impl Sync for MmapSnapshot {}
301#[cfg(not(target_arch = "wasm32"))]
302unsafe impl Send for MmapSnapshot {}
303
304#[cfg(not(target_arch = "wasm32"))]
305impl MmapSnapshot {
306    /// Required file size in bytes for a shared snapshot mapping.
307    pub const FILE_SIZE: usize = size_of::<MmapSnapshot>();
308
309    /// Validate the magic/version header.
310    fn header_valid(&self) -> bool {
311        self.magic == SNAPSHOT_MAGIC && self.version == SNAPSHOT_VERSION
312    }
313
314    /// Publish a full `SnapshotData` under the seqlock.
315    pub fn write(&self, data: &SnapshotData) -> Result<(), String> {
316        if !self.header_valid() {
317            return Err("snapshot file is not initialized (wrong magic/version)".to_string());
318        }
319        let s = self.sequence.fetch_add(1, Ordering::AcqRel); // odd: write in progress
320        std::sync::atomic::fence(Ordering::Release);
321        // SAFETY: sequence is odd, so readers retry; only one writer exists.
322        unsafe {
323            *self.data.get() = *data;
324        }
325        std::sync::atomic::fence(Ordering::Release);
326        self.sequence.store(s + 2, Ordering::Release); // even: stable
327        Ok(())
328    }
329
330    /// Begin an in-place write; returns the token for `end_write`.
331    pub fn begin_write(&self) -> Result<u64, String> {
332        if !self.header_valid() {
333            return Err("snapshot file is not initialized (wrong magic/version)".to_string());
334        }
335        let s = self.sequence.fetch_add(1, Ordering::AcqRel);
336        std::sync::atomic::fence(Ordering::Release);
337        Ok(s + 1)
338    }
339
340    /// End an in-place write started with `begin_write`.
341    pub fn end_write(&self, expected: u64) {
342        std::sync::atomic::fence(Ordering::Release);
343        self.sequence.store(expected + 1, Ordering::Release);
344    }
345
346    /// Exclusive `&mut` access to the data block while the seqlock is held.
347    ///
348    /// # Safety
349    /// Caller must hold the seqlock (`begin_write`, not yet `end_write`).
350    #[allow(clippy::mut_from_ref)]
351    pub unsafe fn data_mut(&self) -> &mut SnapshotData {
352        &mut *self.data.get()
353    }
354
355    /// Read the snapshot; `None` after `SNAPSHOT_MAX_RETRIES` torn reads or
356    /// while the file is not initialized.
357    pub fn read(&self) -> Option<SnapshotData> {
358        if !self.header_valid() {
359            return None;
360        }
361        for _ in 0..SNAPSHOT_MAX_RETRIES {
362            let s1 = self.sequence.load(Ordering::Acquire);
363            if s1 & 1 == 1 {
364                std::hint::spin_loop();
365                continue;
366            }
367            std::sync::atomic::fence(Ordering::Acquire);
368            // SAFETY: sequence is even and stable; torn reads are detected by
369            // the post-read sequence check.
370            let data = unsafe { *self.data.get() };
371            std::sync::atomic::fence(Ordering::Acquire);
372            let s2 = self.sequence.load(Ordering::Acquire);
373            if s1 == s2 {
374                return Some(data);
375            }
376        }
377        None
378    }
379
380    fn from_raw(ptr: *mut u8) -> &'static MmapSnapshot {
381        // SAFETY: callers guarantee the mapping is at least `FILE_SIZE` bytes
382        // and page-aligned (memmap2 mappings are). The seqlock protocol
383        // protects every access to `data`.
384        unsafe { &*(ptr as *const MmapSnapshot) }
385    }
386}
387
388/// Read-only file mapping of a shared consciousness snapshot.
389#[cfg(not(target_arch = "wasm32"))]
390pub struct MappedSnapshot {
391    _file: std::fs::File,
392    _mmap: memmap2::Mmap,
393    shared: &'static MmapSnapshot,
394}
395
396#[cfg(not(target_arch = "wasm32"))]
397impl MappedSnapshot {
398    /// Map an existing snapshot file for reading.
399    pub fn open(path: &std::path::Path) -> Result<Self, String> {
400        let file = std::fs::File::open(path).map_err(|e| format!("open snapshot file: {e}"))?;
401        if file.metadata().map_err(|e| e.to_string())?.len() < MmapSnapshot::FILE_SIZE as u64 {
402            return Err(format!(
403                "snapshot file too small ({} bytes, need {})",
404                file.metadata().map_err(|e| e.to_string())?.len(),
405                MmapSnapshot::FILE_SIZE
406            ));
407        }
408        let mmap =
409            unsafe { memmap2::Mmap::map(&file) }.map_err(|e| format!("mmap snapshot file: {e}"))?;
410        let shared = MmapSnapshot::from_raw(mmap.as_ptr() as *mut u8);
411        Ok(Self {
412            _file: file,
413            _mmap: mmap,
414            shared,
415        })
416    }
417
418    /// Read the latest published snapshot.
419    pub fn read(&self) -> Option<SnapshotData> {
420        self.shared.read()
421    }
422
423    /// True once a writer has initialized the file.
424    pub fn is_initialized(&self) -> bool {
425        self.shared.header_valid()
426    }
427}
428
429/// Writable file mapping of a shared consciousness snapshot (single writer).
430#[cfg(not(target_arch = "wasm32"))]
431pub struct MappedSnapshotWriter {
432    _file: std::fs::File,
433    _mmap: memmap2::MmapMut,
434    shared: &'static MmapSnapshot,
435}
436
437#[cfg(not(target_arch = "wasm32"))]
438impl MappedSnapshotWriter {
439    /// Create (or truncate) the snapshot file and initialize its header.
440    pub fn create(path: &std::path::Path) -> Result<Self, String> {
441        let file = std::fs::OpenOptions::new()
442            .read(true)
443            .write(true)
444            .create(true)
445            .truncate(false)
446            .open(path)
447            .map_err(|e| format!("create snapshot file: {e}"))?;
448        file.set_len(MmapSnapshot::FILE_SIZE as u64)
449            .map_err(|e| format!("size snapshot file: {e}"))?;
450        let mut mmap = unsafe { memmap2::MmapMut::map_mut(&file) }
451            .map_err(|e| format!("mmap snapshot file: {e}"))?;
452        let raw = mmap.as_mut_ptr() as *mut MmapSnapshot;
453        // SAFETY: fresh file, single writer; header is written once before
454        // any reader can observe a valid magic.
455        unsafe {
456            (*raw).magic = SNAPSHOT_MAGIC;
457            (*raw).version = SNAPSHOT_VERSION;
458            (*raw).sequence.store(0, Ordering::Release);
459        }
460        let shared = MmapSnapshot::from_raw(mmap.as_mut_ptr());
461        Ok(Self {
462            _file: file,
463            _mmap: mmap,
464            shared,
465        })
466    }
467
468    /// Open an existing snapshot file for writing, or create a fresh one —
469    /// **without truncating** an existing live mapping.
470    ///
471    /// This is what stream processes use: a second instance must never clobber
472    /// the file out from under the first process's mmap (truncating a mapped
473    /// file invalidates the other mapping and can crash it). Each whole-copy
474    /// `write()` is published under one odd/even seqlock window, so even
475    /// concurrent writers produce complete, non-torn snapshots.
476    pub fn open_or_create(path: &std::path::Path) -> Result<Self, String> {
477        let existing = path.exists();
478        let file = std::fs::OpenOptions::new()
479            .read(true)
480            .write(true)
481            .create(true)
482            .truncate(false)
483            .open(path)
484            .map_err(|e| format!("open snapshot file: {e}"))?;
485
486        if !existing {
487            file.set_len(MmapSnapshot::FILE_SIZE as u64)
488                .map_err(|e| format!("size snapshot file: {e}"))?;
489            let mut mmap = unsafe { memmap2::MmapMut::map_mut(&file) }
490                .map_err(|e| format!("mmap snapshot file: {e}"))?;
491            let raw = mmap.as_mut_ptr() as *mut MmapSnapshot;
492            // SAFETY: fresh file, single initializer; header written before
493            // any reader can observe a valid magic.
494            unsafe {
495                (*raw).magic = SNAPSHOT_MAGIC;
496                (*raw).version = SNAPSHOT_VERSION;
497                (*raw).sequence.store(0, Ordering::Release);
498            }
499            let shared = MmapSnapshot::from_raw(mmap.as_mut_ptr());
500            Ok(Self {
501                _file: file,
502                _mmap: mmap,
503                shared,
504            })
505        } else {
506            if file.metadata().map_err(|e| e.to_string())?.len() < MmapSnapshot::FILE_SIZE as u64 {
507                return Err("snapshot file too small for a shared snapshot".to_string());
508            }
509            let mut mmap = unsafe { memmap2::MmapMut::map_mut(&file) }
510                .map_err(|e| format!("mmap snapshot file: {e}"))?;
511            let shared = MmapSnapshot::from_raw(mmap.as_mut_ptr());
512            if !shared.header_valid() {
513                return Err("snapshot file exists but is not initialized".to_string());
514            }
515            Ok(Self {
516                _file: file,
517                _mmap: mmap,
518                shared,
519            })
520        }
521    }
522
523    /// Open an existing snapshot file for writing.
524    pub fn open(path: &std::path::Path) -> Result<Self, String> {
525        let file = std::fs::OpenOptions::new()
526            .read(true)
527            .write(true)
528            .open(path)
529            .map_err(|e| format!("open snapshot file: {e}"))?;
530        if file.metadata().map_err(|e| e.to_string())?.len() < MmapSnapshot::FILE_SIZE as u64 {
531            return Err("snapshot file too small for a shared snapshot".to_string());
532        }
533        let mut mmap = unsafe { memmap2::MmapMut::map_mut(&file) }
534            .map_err(|e| format!("mmap snapshot file: {e}"))?;
535        let shared = MmapSnapshot::from_raw(mmap.as_mut_ptr());
536        if !shared.header_valid() {
537            return Err("snapshot file is not initialized (wrong magic/version)".to_string());
538        }
539        Ok(Self {
540            _file: file,
541            _mmap: mmap,
542            shared,
543        })
544    }
545
546    /// Publish a full `SnapshotData` and flush the mapping.
547    pub fn write(&self, data: &SnapshotData) -> Result<(), String> {
548        self.shared.write(data)?;
549        let _ = self._mmap.flush();
550        Ok(())
551    }
552
553    /// Begin an in-place write (mirrors `SharedSnapshot::begin_write`).
554    pub fn begin_write(&self) -> Result<u64, String> {
555        self.shared.begin_write()
556    }
557
558    /// End an in-place write.
559    pub fn end_write(&self, expected: u64) {
560        self.shared.end_write(expected);
561    }
562
563    /// Exclusive data access while the seqlock is held.
564    ///
565    /// # Safety
566    /// Caller must hold the seqlock (`begin_write`, not yet `end_write`).
567    #[allow(clippy::mut_from_ref)]
568    pub unsafe fn data_mut(&self) -> &mut SnapshotData {
569        self.shared.data_mut()
570    }
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576
577    #[test]
578    fn seqlock_basic_roundtrip() {
579        let s = SharedSnapshot::new_zeroed();
580        let token = s.begin_write();
581        // Simulate writer: cannot safely mutate through &self from &self
582        // methods in a real scenario; this test exercises the sequence
583        // protocol only.
584        s.end_write(token);
585        assert!(s.read().is_some());
586    }
587
588    #[test]
589    fn seqlock_detects_in_progress_write() {
590        // With a static AtomicU64 we can simulate the protocol from
591        // multiple test sites without unsafe.
592        let s = SharedSnapshot::new_zeroed();
593        let token = s.begin_write();
594        // While the write is in progress (sequence is odd), read should retry.
595        // We assert: sequence is odd now.
596        assert_eq!(s.sequence.load(Ordering::Acquire) & 1, 1);
597        s.end_write(token);
598        assert_eq!(s.sequence.load(Ordering::Acquire) & 1, 0);
599    }
600
601    #[cfg(not(target_arch = "wasm32"))]
602    #[test]
603    fn mmap_snapshot_roundtrip() {
604        let tmp = tempfile::tempdir().unwrap();
605        let path = tmp.path().join("snapshot.bin");
606
607        let writer = MappedSnapshotWriter::create(&path).unwrap();
608        let mut data = SnapshotData::zeroed();
609        data.cycle = 42;
610        data.activations_count = 7;
611        data.surprise_level = 0.25;
612        writer.write(&data).unwrap();
613
614        let reader = MappedSnapshot::open(&path).unwrap();
615        assert!(reader.is_initialized());
616        let read = reader.read().expect("published snapshot must be readable");
617        assert_eq!(read.cycle, 42);
618        assert_eq!(read.activations_count, 7);
619        assert_eq!(read.surprise_level, 0.25);
620    }
621
622    #[cfg(not(target_arch = "wasm32"))]
623    #[test]
624    fn mmap_snapshot_uninitialized_file_reads_none() {
625        let tmp = tempfile::tempdir().unwrap();
626        let path = tmp.path().join("empty.bin");
627        std::fs::write(&path, vec![0u8; MmapSnapshot::FILE_SIZE]).unwrap();
628
629        let reader = MappedSnapshot::open(&path).unwrap();
630        assert!(!reader.is_initialized());
631        assert!(
632            reader.read().is_none(),
633            "uninitialized file must not yield data"
634        );
635    }
636
637    #[cfg(not(target_arch = "wasm32"))]
638    #[test]
639    fn mmap_snapshot_open_or_create_is_non_destructive() {
640        let tmp = tempfile::tempdir().unwrap();
641        let path = tmp.path().join("live.bin");
642
643        // Fresh path: open_or_create initializes it.
644        let writer = MappedSnapshotWriter::open_or_create(&path).unwrap();
645        let mut data = SnapshotData::zeroed();
646        data.cycle = 1;
647        writer.write(&data).unwrap();
648        drop(writer);
649
650        // Existing path: open_or_create must NOT truncate the live mapping.
651        let writer2 = MappedSnapshotWriter::open_or_create(&path).unwrap();
652        let mut data2 = SnapshotData::zeroed();
653        data2.cycle = 2;
654        writer2.write(&data2).unwrap();
655
656        let reader = MappedSnapshot::open(&path).unwrap();
657        assert_eq!(reader.read().unwrap().cycle, 2);
658    }
659
660    #[cfg(not(target_arch = "wasm32"))]
661    #[test]
662    fn mmap_snapshot_no_torn_reads_under_concurrency() {
663        let tmp = tempfile::tempdir().unwrap();
664        let path = tmp.path().join("live.bin");
665        let writer = MappedSnapshotWriter::create(&path).unwrap();
666
667        let writer_handle = std::thread::spawn(move || {
668            for cycle in 1..=200u64 {
669                let mut data = SnapshotData::zeroed();
670                data.cycle = cycle;
671                // Distinct marker so a torn read would be detectable:
672                data.activations_count = (cycle * 7) as u32;
673                writer.write(&data).unwrap();
674            }
675        });
676
677        let reader = MappedSnapshot::open(&path).unwrap();
678        let mut last_seen = 0u64;
679        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
680        while last_seen < 200 && std::time::Instant::now() < deadline {
681            if let Some(data) = reader.read() {
682                // Every successful read must be internally consistent:
683                assert_eq!(
684                    data.activations_count,
685                    (data.cycle * 7) as u32,
686                    "torn read observed: cycle={} count={}",
687                    data.cycle,
688                    data.activations_count
689                );
690                last_seen = last_seen.max(data.cycle);
691            }
692        }
693        writer_handle.join().unwrap();
694        assert_eq!(last_seen, 200, "reader must observe the final snapshot");
695    }
696}