From 624f8425e1103a29f7c329ddb7302f9ee52f78e5 Mon Sep 17 00:00:00 2001 From: ApiliumDevTeam Date: Tue, 21 Jul 2026 12:37:22 +0200 Subject: [PATCH 1/2] fix(ineru): STM tolerates a single entry larger than its byte budget (#131) Bulk ingest of a large note/file produces one MemoryEntry whose serialized size alone exceeds the STM memory budget (1MB in the standard profile). store() tried to prune space for it, but an entry that alone overflows the budget can never be made to fit, so once the STM emptied it returned a hard "STM memory capacity exceeded" error and aborted the whole ingest ("Motor no disponible"). The byte budget is a pruning target, not an admission gate: STM is a transient buffer that consolidates into LTM (which is bounded by count, not bytes). store() now prunes only while pruning makes progress and admits the entry over budget once it cannot free more. This also fixes a latent infinite loop: when every resident entry was already consolidated (prune_one leaves those in place) the prune loop spun forever. prune_one now returns whether it evicted anything so store() can stop. Covered by test_oversized_entry_is_accepted_not_rejected and test_store_terminates_when_only_consolidated_entries_remain. --- crates/ineru/src/stm.rs | 90 ++++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 15 deletions(-) diff --git a/crates/ineru/src/stm.rs b/crates/ineru/src/stm.rs index 435a3898..1595770b 100644 --- a/crates/ineru/src/stm.rs +++ b/crates/ineru/src/stm.rs @@ -9,7 +9,7 @@ //! capacity. use crate::config::StmConfig; -use crate::error::{Error, Result}; +use crate::error::Result; use crate::types::{MemoryEntry, MemoryId, MemoryQuery, MemoryResult, MemorySource, Timestamp}; use std::collections::HashMap; @@ -56,19 +56,22 @@ impl ShortTermMemory { // Check entry count capacity if self.entries.len() >= self.config.max_entries { - self.prune_one()?; + self.prune_one(); } - // Check memory limit + // The byte budget is a pruning target, not a hard admission gate. Evict + // the least-important prunable entries while that keeps freeing space. + // We stop as soon as pruning can make no further progress — either STM + // is empty, or the only residents are already consolidated (which + // prune_one leaves in place). A single entry larger than the whole + // budget (a big note/file during bulk ingest) is then admitted over + // budget rather than bricking the write; consolidation moves it to LTM + // and it is evicted from STM. This must never loop forever or return a + // hard "STM memory capacity exceeded" error. while self.memory_usage + entry_size > self.config.max_memory_bytes { - if self.entries.is_empty() { - return Err(Error::capacity( - "STM memory", - entry_size, - self.config.max_memory_bytes, - )); + if !self.prune_one() { + break; } - self.prune_one()?; } // Store the entry @@ -209,8 +212,13 @@ impl ShortTermMemory { Ok(count) } - /// Prunes a single entry with the lowest attention score that has not been consolidated. - fn prune_one(&mut self) -> Result<()> { + /// Prunes a single entry with the lowest attention score that has not been + /// consolidated. + /// + /// Returns `true` if an entry was evicted, `false` if there was nothing + /// prunable (empty STM, or every resident is already consolidated). Callers + /// use the return value to stop pruning once it can make no more progress. + fn prune_one(&mut self) -> bool { // Find entry with lowest attention that hasn't been consolidated let to_remove = self .entries @@ -225,10 +233,11 @@ impl ShortTermMemory { .map(|(id, _)| id.clone()); if let Some(id) = to_remove { - self.remove(&id)?; + let _ = self.remove(&id); + true + } else { + false } - - Ok(()) } /// Retrieves a list of memory entries that are candidates for consolidation into LTM. @@ -408,6 +417,57 @@ mod tests { assert!(stm.len() <= 2); } + #[test] + fn test_oversized_entry_is_accepted_not_rejected() { + // A single entry larger than the whole STM byte budget must still be + // stored (over budget) rather than bricking the write. This is the + // bulk-ingest "STM memory capacity exceeded" crash: a big note/file + // becomes one memory entry that alone exceeds max_memory_bytes, so + // pruning can never make room. STM is a transient buffer; consolidation + // moves the entry to LTM, so admitting it over budget is correct. + let config = StmConfig { + max_memory_bytes: 1024, // 1KB budget + ..Default::default() + }; + let mut stm = ShortTermMemory::new(config); + + let big = "x".repeat(8 * 1024); // ~8KB payload, far over budget + let entry = MemoryEntry::new("doc", serde_json::json!({ "body": big })); + let id = stm + .store(entry) + .expect("oversized entry must be accepted, not rejected"); + + assert!(stm.get(&id).unwrap().is_some()); + } + + #[test] + fn test_store_terminates_when_only_consolidated_entries_remain() { + // If STM is over budget but every resident entry is already consolidated + // (prune_one only evicts non-consolidated entries), a new store must + // still terminate and succeed instead of spinning forever. + let config = StmConfig { + max_memory_bytes: 4 * 1024, + ..Default::default() + }; + let mut stm = ShortTermMemory::new(config); + + let payload = "y".repeat(3 * 1024); + let id = stm + .store(MemoryEntry::new( + "a", + serde_json::json!({ "b": payload.clone() }), + )) + .unwrap(); + stm.mark_consolidated(&id).unwrap(); + + // Second store pushes over budget; the only prunable candidate is + // consolidated, so pruning stalls — store must break out and accept. + let id2 = stm + .store(MemoryEntry::new("c", serde_json::json!({ "b": payload }))) + .unwrap(); + assert!(stm.get(&id2).unwrap().is_some()); + } + #[test] fn test_query_by_type() { let config = StmConfig::default(); From 397208cbb7437ed6289c7ac20cb3e25fc4c51c95 Mon Sep 17 00:00:00 2001 From: ApiliumDevTeam Date: Tue, 21 Jul 2026 13:50:08 +0200 Subject: [PATCH 2/2] release: aingle 0.7.6 Per-crate version bumps for the changes landed since v0.7.5: - ineru 0.7.4 -> 0.7.5: short-term memory admits an entry larger than its byte budget instead of erroring, so bulk ingest never stalls. - aingle_ingest 0.7.1 -> 0.7.2: byte-bounded chunking; large/minified files no longer produce a single oversized chunk. - aingle_cortex 0.7.5 -> 0.7.6: aingle_path (shortest verified connection between two notes) and the ingest improvements above. Dependents pin these with caret "0.7", so the patch/minor bumps resolve without changes. Cargo.lock refreshed. --- Cargo.lock | 6 +++--- crates/aingle_cortex/Cargo.toml | 2 +- crates/aingle_ingest/Cargo.toml | 2 +- crates/ineru/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 477f40bb..d6e4ec9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,7 +140,7 @@ dependencies = [ [[package]] name = "aingle_cortex" -version = "0.7.5" +version = "0.7.6" dependencies = [ "aingle_graph", "aingle_ingest", @@ -223,7 +223,7 @@ dependencies = [ [[package]] name = "aingle_ingest" -version = "0.7.1" +version = "0.7.2" dependencies = [ "aingle_graph", "blake3", @@ -4262,7 +4262,7 @@ dependencies = [ [[package]] name = "ineru" -version = "0.7.4" +version = "0.7.5" dependencies = [ "bincode", "blake3", diff --git a/crates/aingle_cortex/Cargo.toml b/crates/aingle_cortex/Cargo.toml index 1f5dd94d..0ff81fb3 100644 --- a/crates/aingle_cortex/Cargo.toml +++ b/crates/aingle_cortex/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_cortex" -version = "0.7.5" +version = "0.7.6" description = "Córtex API - REST/GraphQL/SPARQL interface for AIngle semantic graphs" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" diff --git a/crates/aingle_ingest/Cargo.toml b/crates/aingle_ingest/Cargo.toml index 8de6927a..563ff993 100644 --- a/crates/aingle_ingest/Cargo.toml +++ b/crates/aingle_ingest/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_ingest" -version = "0.7.1" +version = "0.7.2" description = "Structural extraction of triples and text chunks from markdown/code for AIngle" license = "Apache-2.0 OR LicenseRef-Commercial" edition = "2021" diff --git a/crates/ineru/Cargo.toml b/crates/ineru/Cargo.toml index e4407b49..850a50c4 100644 --- a/crates/ineru/Cargo.toml +++ b/crates/ineru/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ineru" -version = "0.7.4" +version = "0.7.5" description = "Ineru: Neural-inspired memory system for AIngle AI agents" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle"