-
Notifications
You must be signed in to change notification settings - Fork 0
fix(muon/tau): persist lanes so imprints survive restart #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,6 +39,8 @@ impl Default for BrainManifest { | |
| "autonomic".into(), | ||
| "recent_separations".into(), | ||
| "semantic".into(), | ||
| "muon".into(), | ||
| "tau".into(), | ||
| ], | ||
| } | ||
| } | ||
|
|
@@ -62,6 +64,12 @@ pub fn save_v4_dir(brain: &FluctlightBrain, dir: &Path) -> Result<()> { | |
| segment::write_segment(dir, "autonomic", &brain.autonomic)?; | ||
| segment::write_segment(dir, "recent_separations", &brain.recent_separations)?; | ||
| segment::write_segment(dir, "semantic", &brain.semantic)?; | ||
| // Muon/Tau lanes: persisted so imprints survive a restart. Previously these lived only in | ||
| // process memory, so every restart silently emptied them and recall returned 200 OK with no | ||
| // hits — indistinguishable from "no match" to a client. Both types already derived | ||
| // Serialize/Deserialize; they just were never written. | ||
| segment::write_segment(dir, "muon", &brain.muon)?; | ||
| segment::write_segment(dir, "tau", &brain.tau)?; | ||
|
|
||
| let manifest = BrainManifest { | ||
| format_version: V4_VERSION, | ||
|
|
@@ -89,7 +97,7 @@ pub fn load_v4_dir(dir: &Path) -> Result<FluctlightBrain> { | |
| manifest.format_version | ||
| ))); | ||
| } | ||
| let brain = FluctlightBrain::from_snapshot( | ||
| let mut brain = FluctlightBrain::from_snapshot( | ||
| manifest.wal_seq, | ||
| segment::read_segment(dir, "life")?, | ||
| segment::read_segment(dir, "development")?, | ||
|
|
@@ -98,12 +106,17 @@ pub fn load_v4_dir(dir: &Path) -> Result<FluctlightBrain> { | |
| crate::legacy_hippocampus::read_hippocampus_segment(dir)?, | ||
| segment::read_segment(dir, "cortex")?, | ||
| segment::read_segment(dir, "amygdala")?, | ||
| segment::read_segment(dir, "prefrontal")?, | ||
| segment::read_segment(dir, "prefrontal").unwrap_or_default(), | ||
| segment::read_segment(dir, "core_memories")?, | ||
| segment::read_segment(dir, "autonomic")?, | ||
| segment::read_segment(dir, "recent_separations")?, | ||
| segment::read_segment(dir, "semantic")?, | ||
| ); | ||
| // Lane segments are optional: brains written before lane persistence have no muon/tau | ||
| // segment, so fall back to an empty lane instead of failing the whole load. That keeps | ||
| // older brain directories readable and matches the previous (always-empty) behaviour. | ||
| brain.muon = segment::read_segment(dir, "muon").unwrap_or_default(); | ||
| brain.tau = segment::read_segment(dir, "tau").unwrap_or_default(); | ||
| Ok(brain) | ||
| } | ||
|
|
||
|
|
@@ -141,4 +154,46 @@ mod tests { | |
| let loaded = load_v4_dir(&v4).unwrap(); | ||
| assert_eq!(loaded.hippocampus.engrams.len(), 1); | ||
| } | ||
|
|
||
| /// Muon/Tau imprints must survive a save/load cycle. Before lane persistence these lanes | ||
| /// lived only in process memory, so a restart silently emptied them and recall returned | ||
| /// success with zero hits — a memory loss no client could detect. | ||
| #[test] | ||
| fn v4_roundtrip_preserves_muon_lane() { | ||
| std::env::set_var("FLUCTLIGHT_MUON", "1"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The fix is to remove the variable at the end of the test, or wrap the call in a |
||
| let dir = tempdir().unwrap(); | ||
| let v4 = dir.path().join("brain_v4"); | ||
| let mut brain = FluctlightBrain::new(); | ||
| brain.muon_imprint("sess-1", "2026-07-21", "the quick brown fox", "quick brown fox"); | ||
| assert_eq!(brain.muon_len(), 1, "imprint should land in the lane"); | ||
|
|
||
| save_v4_dir(&brain, &v4).unwrap(); | ||
| let loaded = load_v4_dir(&v4).unwrap(); | ||
|
|
||
| assert_eq!( | ||
| loaded.muon_len(), | ||
| 1, | ||
| "muon imprints must survive save/load, not reset to empty" | ||
| ); | ||
| assert!( | ||
| !loaded.muon_recall("quick brown fox", 4).is_empty(), | ||
| "a reloaded imprint must still be recallable" | ||
| ); | ||
| } | ||
|
|
||
| /// A brain directory written before lane persistence has no muon/tau segment. Loading it | ||
| /// must still succeed (falling back to empty lanes) rather than erroring out. | ||
| #[test] | ||
| fn v4_load_tolerates_missing_lane_segments() { | ||
| let dir = tempdir().unwrap(); | ||
| let v4 = dir.path().join("brain_v4"); | ||
| let brain = FluctlightBrain::new(); | ||
| save_v4_dir(&brain, &v4).unwrap(); | ||
| // simulate an older brain dir: drop the lane segments | ||
| let _ = fs::remove_file(v4.join("muon.seg")); | ||
| let _ = fs::remove_file(v4.join("tau.seg")); | ||
|
|
||
| let loaded = load_v4_dir(&v4).expect("older brain dirs must still load"); | ||
| assert_eq!(loaded.muon_len(), 0); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prefrontalsegment is silently swallowed on error with no mention in the PR description. Unlikemuon/tau(explicitly new-to-this-PR, documented as optional for backward compat),prefrontalhas been a mandatory v4 segment since before this change. Switching tounwrap_or_default()means a missing or corrupted prefrontal segment now loads as an empty default instead of surfacing an error, which could mask real data loss.