From 276b3ea8d17a1166269107296508225f429968e8 Mon Sep 17 00:00:00 2001 From: rUv Date: Mon, 23 Mar 2026 20:33:40 +0000 Subject: [PATCH] =?UTF-8?q?feat(sona):=20complete=20state=20persistence=20?= =?UTF-8?q?=E2=80=94=20loadState()=20restores=20patterns=20(#274)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes #274 by adding the load path for SONA state persistence: - ReasoningBank.insert_pattern(): directly insert a pattern (for restore) - LoopCoordinator.load_state(json): deserialize and restore patterns - NAPI loadState(stateJson): binding for Node.js - TypeScript loadState(stateJson): wrapper with return count Full save/load cycle now works: const state = engine.saveState(); // serialize patterns to JSON // ... restart ... const restored = engine.loadState(state); // restore N patterns serialize_state() now includes full pattern data (centroids, quality, cluster sizes) not just counts. Co-Authored-By: claude-flow --- crates/sona/src/loops/coordinator.rs | 28 +++++++++++++++++-- crates/sona/src/napi.rs | 14 ++++++++++ crates/sona/src/reasoning_bank.rs | 10 +++++++ .../ruvector/src/core/sona-wrapper.ts | 9 ++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/crates/sona/src/loops/coordinator.rs b/crates/sona/src/loops/coordinator.rs index e2f006e5f..927a162e9 100644 --- a/crates/sona/src/loops/coordinator.rs +++ b/crates/sona/src/loops/coordinator.rs @@ -162,17 +162,41 @@ impl LoopCoordinator { } } - /// Serialize state to JSON for persistence (fixes #274) + /// Serialize full state to JSON for persistence (fixes #274) pub fn serialize_state(&self) -> String { let rb = self.reasoning_bank.read(); + let patterns = rb.get_all_patterns(); let ewc = self.ewc.read(); serde_json::json!({ - "reasoning_bank_patterns": rb.pattern_count(), + "version": 1, + "patterns": patterns, "ewc_task_count": ewc.task_count(), "instant_enabled": self.instant_enabled, "background_enabled": self.background_enabled, }).to_string() } + + /// Restore state from JSON (fixes #274) + /// Call after construction to restore learned patterns from a previous session. + pub fn load_state(&self, json: &str) -> Result { + let state: serde_json::Value = serde_json::from_str(json) + .map_err(|e| format!("Invalid state JSON: {}", e))?; + + let mut loaded = 0; + + // Restore patterns into reasoning bank + if let Some(patterns) = state.get("patterns").and_then(|p| p.as_array()) { + let mut rb = self.reasoning_bank.write(); + for p in patterns { + if let Ok(pattern) = serde_json::from_value::(p.clone()) { + rb.insert_pattern(pattern); + loaded += 1; + } + } + } + + Ok(loaded) + } } /// Coordinator statistics diff --git a/crates/sona/src/napi.rs b/crates/sona/src/napi.rs index 9cce18267..7ecd52937 100644 --- a/crates/sona/src/napi.rs +++ b/crates/sona/src/napi.rs @@ -144,6 +144,20 @@ impl SonaEngine { self.inner.coordinator().serialize_state() } + /// Restore engine state from JSON (fixes #274) + /// @param state_json - JSON string from saveState() + /// @returns Number of patterns restored + #[napi] + pub fn load_state(&self, state_json: String) -> u32 { + match self.inner.coordinator().load_state(&state_json) { + Ok(count) => count as u32, + Err(e) => { + eprintln!("SONA load_state error: {}", e); + 0 + } + } + } + /// Enable or disable the engine /// @param enabled - Whether to enable the engine #[napi] diff --git a/crates/sona/src/reasoning_bank.rs b/crates/sona/src/reasoning_bank.rs index 932f0818b..7038e8c7c 100644 --- a/crates/sona/src/reasoning_bank.rs +++ b/crates/sona/src/reasoning_bank.rs @@ -406,6 +406,16 @@ impl ReasoningBank { self.patterns.values().cloned().collect() } + /// Insert a pattern directly (for state restoration, fixes #274) + pub fn insert_pattern(&mut self, pattern: LearnedPattern) { + let id = pattern.id; + if id >= self.next_pattern_id { + self.next_pattern_id = id + 1; + } + self.pattern_index.push((pattern.centroid.clone(), id)); + self.patterns.insert(id, pattern); + } + /// Consolidate similar patterns pub fn consolidate(&mut self, similarity_threshold: f32) { let pattern_ids: Vec = self.patterns.keys().copied().collect(); diff --git a/npm/packages/ruvector/src/core/sona-wrapper.ts b/npm/packages/ruvector/src/core/sona-wrapper.ts index d2e670d6f..06508a8db 100644 --- a/npm/packages/ruvector/src/core/sona-wrapper.ts +++ b/npm/packages/ruvector/src/core/sona-wrapper.ts @@ -353,6 +353,15 @@ export class SonaEngine { return this._native.saveState(); } + /** + * Restore engine state from JSON saved by saveState() (fixes #274) + * @param stateJson - JSON string from a previous saveState() call + * @returns Number of patterns restored + */ + loadState(stateJson: string): number { + return this._native.loadState(stateJson); + } + /** * Enable or disable the engine * @param enabled Whether to enable