From 9daeede5aa88fa3820467add52685c1cf4be7dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Samek?= Date: Fri, 24 Apr 2026 16:07:45 +0200 Subject: [PATCH 1/2] Fix layer-store snapshot path (#15) Add layer_path arg to trie_snapshot/trie_restore; LayerStore::save now creates parent dir. Unblocks ubuntu CI and stops tests polluting the global layer store. --- src/mcp/tools.rs | 28 +++++++++++++++++++--------- src/store/layer.rs | 5 +++++ tests/honest_agent_scenarios.rs | 8 ++++++-- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/mcp/tools.rs b/src/mcp/tools.rs index b5f1380..2e97a93 100644 --- a/src/mcp/tools.rs +++ b/src/mcp/tools.rs @@ -164,25 +164,27 @@ pub fn tool_list() -> Value { }, { "name": "trie_snapshot", - "description": "Save both tries (byte + word) and content store to disk.", + "description": "Save both tries (byte + word), content store, and layer store to disk.", "inputSchema": { "type": "object", "properties": { "path": { "type": "string", "description": "Byte-trie file path (default: ./trie-memory.dat)" }, "word_path": { "type": "string", "description": "Word-trie file path (default: ./word-trie-memory.dat)" }, - "content_path": { "type": "string", "description": "Content-store file path (default: ./content-store.json)" } + "content_path": { "type": "string", "description": "Content-store file path (default: ./content-store.json)" }, + "layer_path": { "type": "string", "description": "Layer-store file path (default: ./layer-store.json)" } } } }, { "name": "trie_restore", - "description": "Restore both tries (byte + word) and content store from disk.", + "description": "Restore both tries (byte + word), content store, and layer store from disk.", "inputSchema": { "type": "object", "properties": { "path": { "type": "string", "description": "Byte-trie file path (default: ./trie-memory.dat)" }, "word_path": { "type": "string", "description": "Word-trie file path (default: ./word-trie-memory.dat)" }, - "content_path": { "type": "string", "description": "Content-store file path (default: ./content-store.json)" } + "content_path": { "type": "string", "description": "Content-store file path (default: ./content-store.json)" }, + "layer_path": { "type": "string", "description": "Layer-store file path (default: ./layer-store.json)" } } } }, @@ -987,6 +989,10 @@ fn handle_snapshot(trie: &Trie, word_trie: &Trie, store: &ContentStore, layers: .get("content_path") .and_then(|v| v.as_str()) .unwrap_or(DEFAULT_CONTENT_PATH); + let layer_path = args + .get("layer_path") + .and_then(|v| v.as_str()) + .unwrap_or(DEFAULT_LAYER_PATH); let byte = match trie.snapshot(byte_path) { Ok(r) => r, @@ -1000,7 +1006,7 @@ fn handle_snapshot(trie: &Trie, word_trie: &Trie, store: &ContentStore, layers: Ok(n) => n, Err(e) => return tool_error(&format!("Content-store snapshot failed: {}", e)), }; - let layer_bytes = match layers.save(DEFAULT_LAYER_PATH) { + let layer_bytes = match layers.save(layer_path) { Ok(n) => n, Err(e) => return tool_error(&format!("Layer-store snapshot failed: {}", e)), }; @@ -1009,7 +1015,7 @@ fn handle_snapshot(trie: &Trie, word_trie: &Trie, store: &ContentStore, layers: "byte": { "path": byte.path, "bytes_written": byte.bytes_written }, "word": { "path": word.path, "bytes_written": word.bytes_written }, "content": { "path": content_path, "bytes_written": content_bytes }, - "layers": { "path": DEFAULT_LAYER_PATH, "bytes_written": layer_bytes }, + "layers": { "path": layer_path, "bytes_written": layer_bytes }, })) } @@ -1026,6 +1032,10 @@ fn handle_restore(trie: &mut Trie, word_trie: &mut Trie, store: &mut ContentStor .get("content_path") .and_then(|v| v.as_str()) .unwrap_or(DEFAULT_CONTENT_PATH); + let layer_path = args + .get("layer_path") + .and_then(|v| v.as_str()) + .unwrap_or(DEFAULT_LAYER_PATH); let byte_restored = match Trie::restore_from(byte_path) { Ok(r) => r, @@ -1056,8 +1066,8 @@ fn handle_restore(trie: &mut Trie, word_trie: &mut Trie, store: &mut ContentStor }; // Restore layer store if file exists - let layer_count = if LayerStore::exists(DEFAULT_LAYER_PATH) { - match LayerStore::load(DEFAULT_LAYER_PATH) { + let layer_count = if LayerStore::exists(layer_path) { + match LayerStore::load(layer_path) { Ok(restored) => { let count = restored.count(); *layers = restored; @@ -1073,7 +1083,7 @@ fn handle_restore(trie: &mut Trie, word_trie: &mut Trie, store: &mut ContentStor "byte": { "path": byte_path, "nodes_restored": byte_count }, "word": { "path": word_path, "nodes_restored": word_count }, "content": { "path": content_path, "entries_restored": content_entries }, - "layers": { "path": DEFAULT_LAYER_PATH, "layers_restored": layer_count }, + "layers": { "path": layer_path, "layers_restored": layer_count }, })) } diff --git a/src/store/layer.rs b/src/store/layer.rs index 2fc4944..2c3b55e 100644 --- a/src/store/layer.rs +++ b/src/store/layer.rs @@ -133,6 +133,11 @@ impl LayerStore { let json = serde_json::to_string_pretty(self) .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; let len = json.len(); + if let Some(parent) = std::path::Path::new(path).parent() { + if !parent.as_os_str().is_empty() { + std::fs::create_dir_all(parent)?; + } + } std::fs::write(path, &json)?; Ok(len) } diff --git a/tests/honest_agent_scenarios.rs b/tests/honest_agent_scenarios.rs index 5799f6c..31fc84b 100644 --- a/tests/honest_agent_scenarios.rs +++ b/tests/honest_agent_scenarios.rs @@ -202,23 +202,25 @@ fn scenario_2_within_session_learning() { // ---------- Scenario 3 ---------- -fn snapshot_paths(suffix: &str) -> (String, String, String) { +fn snapshot_paths(suffix: &str) -> (String, String, String, String) { let dir = std::env::temp_dir().join(format!("trie-memory-scenario-{}", suffix)); std::fs::create_dir_all(&dir).ok(); ( dir.join("trie.dat").to_string_lossy().into_owned(), dir.join("word-trie.dat").to_string_lossy().into_owned(), dir.join("content.json").to_string_lossy().into_owned(), + dir.join("layer.json").to_string_lossy().into_owned(), ) } #[test] fn scenario_3_cross_session_learning() { - let (trie_path, word_path, content_path) = snapshot_paths("cross-session"); + let (trie_path, word_path, content_path, layer_path) = snapshot_paths("cross-session"); // Ensure clean start. std::fs::remove_file(&trie_path).ok(); std::fs::remove_file(&word_path).ok(); std::fs::remove_file(&content_path).ok(); + std::fs::remove_file(&layer_path).ok(); // ---- Session 1: prime, teach, snapshot ---- { @@ -241,6 +243,7 @@ fn scenario_3_cross_session_learning() { "path": trie_path, "word_path": word_path, "content_path": content_path, + "layer_path": layer_path, }), ); assert!( @@ -259,6 +262,7 @@ fn scenario_3_cross_session_learning() { "path": trie_path.clone(), "word_path": word_path.clone(), "content_path": content_path.clone(), + "layer_path": layer_path.clone(), }), ); assert!( From 96eb1dca694b0c81d5637289d0c0b72263c6e0f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Samek?= Date: Fri, 24 Apr 2026 17:36:38 +0200 Subject: [PATCH 2/2] Also create parent dirs in Trie/ContentStore/ConceptStore save Same ENOENT class as #15: tests that use default paths hit nonexistent W:/data/trie-store/ on CI. --- src/store/concept.rs | 5 +++++ src/store/mod.rs | 5 +++++ src/trie/persistence.rs | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/src/store/concept.rs b/src/store/concept.rs index 2fdea13..17b6c92 100644 --- a/src/store/concept.rs +++ b/src/store/concept.rs @@ -346,6 +346,11 @@ impl ConceptStore { pub fn snapshot(&self, path: &str) -> Result { let json = serde_json::to_string_pretty(self).map_err(|e| e.to_string())?; let len = json.len(); + if let Some(parent) = std::path::Path::new(path).parent() { + if !parent.as_os_str().is_empty() { + std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + } std::fs::write(path, &json).map_err(|e| e.to_string())?; Ok(len) } diff --git a/src/store/mod.rs b/src/store/mod.rs index f0c4035..ab79d39 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -174,6 +174,11 @@ impl ContentStore { let json = serde_json::to_string_pretty(self) .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; let len = json.len(); + if let Some(parent) = std::path::Path::new(path).parent() { + if !parent.as_os_str().is_empty() { + std::fs::create_dir_all(parent)?; + } + } std::fs::write(path, &json)?; Ok(len) } diff --git a/src/trie/persistence.rs b/src/trie/persistence.rs index 82d9def..4334110 100644 --- a/src/trie/persistence.rs +++ b/src/trie/persistence.rs @@ -21,6 +21,11 @@ impl Trie { pub fn snapshot(&self, path: &str) -> io::Result { let bytes = bincode::serialize(self).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; let len = bytes.len(); + if let Some(parent) = Path::new(path).parent() { + if !parent.as_os_str().is_empty() { + fs::create_dir_all(parent)?; + } + } fs::write(path, &bytes)?; Ok(SnapshotResult { path: path.to_string(),