diff --git a/crates/fluctlightdb/src/manifest.rs b/crates/fluctlightdb/src/manifest.rs index 63c36fec..002945d6 100644 --- a/crates/fluctlightdb/src/manifest.rs +++ b/crates/fluctlightdb/src/manifest.rs @@ -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 { 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 { 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"); + 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); + } } diff --git a/crates/fluctlightdb/src/muon_runtime.rs b/crates/fluctlightdb/src/muon_runtime.rs index 4525b56d..1b2244b0 100644 --- a/crates/fluctlightdb/src/muon_runtime.rs +++ b/crates/fluctlightdb/src/muon_runtime.rs @@ -30,18 +30,26 @@ impl FluctlightBrain { } else { self.muon.imprint(session_id, date, body, user_keys); } + // Count the imprint as a write so it participates in normal checkpointing. Without this + // the lane is only ever mutated in memory: it is never WAL-logged and never marks the + // brain dirty, so persisting the lane in the manifest alone still loses every imprint on + // restart. Errors are swallowed deliberately — a checkpoint failure must not make an + // imprint look like it failed, and the next checkpoint will retry. + let _ = self.maybe_checkpoint(); } pub fn muon_imprint_batch(&mut self, sessions: &[MuonImprintInput]) -> usize { if !muon_enabled() { return 0; } - if tau_enabled() { + let n = if tau_enabled() { let (n, _) = self.tau.imprint_batch(sessions); n } else { self.muon.imprint_batch(sessions) - } + }; + let _ = self.maybe_checkpoint(); + n } /// Penetrative recall — Tau episodic fission when `FLUCTLIGHT_TAU=1`, else session Muon hits. diff --git a/crates/fluctlightdb/src/serve.rs b/crates/fluctlightdb/src/serve.rs index 70e1044e..b15fd161 100644 --- a/crates/fluctlightdb/src/serve.rs +++ b/crates/fluctlightdb/src/serve.rs @@ -1245,6 +1245,75 @@ fn dispatch( Ok(serde_json::json!({"ok": true, "engram_id": eid.to_string()})) }) } + // ── CHORUS lane ────────────────────────────────────────────────────────────── + // The MaxSim⊕BM25 late-interaction stack was reachable only through the native/SDK + // API, which is unusable while `serve` holds the exclusive brain lock — so any + // client talking HTTP could not use it at all. These endpoints expose it. + "/api/v1/chorus/imprint" | "/chorus/imprint" => { + require_writable(server)?; + require_role(auth, Role::Write)?; + if !crate::chorus_runtime::chorus_enabled() { + return Err(Error::Store( + "FLUCTLIGHT_CHORUS=1 required for CHORUS imprint".into(), + )); + } + let content = api_body + .content + .as_deref() + .ok_or_else(|| Error::Store("missing content".into()))?; + let memory_id = api_body + .doc_id + .as_deref() + .or(api_body.key.as_deref()) + .ok_or_else(|| Error::Store("missing doc_id (memory_id)".into()))?; + let input = crate::chorus::ChorusImprintInput { + memory_id: memory_id.to_string(), + content: content.to_string(), + context: api_body.context.clone().unwrap_or_default(), + semantic_vector: api_body.semantic_vector.clone(), + token_vectors: None, + salience: api_body.salience.unwrap_or(0.7), + sheath: Default::default(), + }; + server.with_brain_write(tenant_id, |b| { + let ok = b.chorus_imprint(&input); + Ok(serde_json::json!({ + "ok": ok, + "memory_id": memory_id, + "chorus_len": b.chorus_len(), + })) + }) + } + "/api/v1/chorus/recall" | "/chorus/recall" => { + require_role(auth, Role::Read)?; + if !crate::chorus_runtime::chorus_enabled() { + return Err(Error::Store( + "FLUCTLIGHT_CHORUS=1 required for CHORUS recall".into(), + )); + } + let cue = api_body + .cue + .as_deref() + .or(api_body.content.as_deref()) + .ok_or_else(|| Error::Store("missing cue".into()))?; + let k = api_body.limit.unwrap_or(8).min(64); + let cue_vector = api_body.semantic_vector.as_deref(); + server.with_brain_read(tenant_id, |b| { + Ok(serde_json::json!({ + "hits": b.chorus_recall(cue, k, cue_vector), + "chorus_len": b.chorus_len(), + })) + }) + } + "/api/v1/chorus/stats" | "/chorus/stats" => { + require_role(auth, Role::Read)?; + server.with_brain_read(tenant_id, |b| { + Ok(serde_json::json!({ + "enabled": crate::chorus_runtime::chorus_enabled(), + "chorus_len": b.chorus_len(), + })) + }) + } "/api/v1/export-graph-lite" | "/export-graph-lite" => { require_role(auth, Role::Read)?; server.with_brain_read(tenant_id, |b| { diff --git a/crates/fluctlightdb/src/viewer.html b/crates/fluctlightdb/src/viewer.html index c2dc5654..68f7439b 100644 --- a/crates/fluctlightdb/src/viewer.html +++ b/crates/fluctlightdb/src/viewer.html @@ -2,82 +2,131 @@ - + FluctlightDB · Living Brain + - +
-
+
+ FluctlightDB Living Brain · real-time connectome
-
offline -
+
+ offline + + + +
-
-

Vitals

+
+

Vitals

0
Engrams
0
Synapses
@@ -133,43 +260,97 @@
synapse pressure0%
+ +
-