Skip to main content

llm_agent_runtime/
memory.rs

1//! # Module: Memory
2//!
3//! ## Responsibility
4//! Provides episodic, semantic, and working memory stores for agents.
5//! Mirrors the public API of `tokio-agent-memory` and `tokio-memory`.
6//!
7//! ## Guarantees
8//! - Thread-safe: all stores wrap their state in `Arc<Mutex<_>>`
9//! - Bounded: WorkingMemory evicts the oldest entry when capacity is exceeded
10//! - Decaying: DecayPolicy reduces importance scores over time
11//! - Non-panicking: all operations return `Result`
12//! - Lock-poisoning resilient: a panicking thread does not permanently break a store
13//! - O(1) agent lookup: EpisodicStore indexes items per-agent for efficient recall
14//!
15//! ## NOT Responsible For
16//! - Cross-agent shared memory (see runtime.rs coordinator)
17//! - Persistence to disk or external store
18
19use crate::error::AgentRuntimeError;
20use crate::util::recover_lock;
21use chrono::{DateTime, Utc};
22use serde::{Deserialize, Serialize};
23use std::collections::{HashMap, VecDeque};
24use std::sync::{Arc, Mutex};
25
26// Re-export the core ID types so callers can import from either module.
27pub use crate::types::{AgentId, MemoryId};
28
29// ── Cosine similarity ─────────────────────────────────────────────────────────
30
31/// Normalize `v` to unit length in-place.  Does nothing if the norm is below
32/// `f32::EPSILON` (zero or near-zero vector).
33fn normalize_in_place(v: &mut Vec<f32>) {
34    let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
35    if norm > f32::EPSILON {
36        for x in v.iter_mut() {
37            *x /= norm;
38        }
39    }
40}
41
42// ── MemoryItem ────────────────────────────────────────────────────────────────
43
44/// A single memory record stored for an agent.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct MemoryItem {
47    /// Unique identifier for this memory.
48    pub id: MemoryId,
49    /// The agent this memory belongs to.
50    pub agent_id: AgentId,
51    /// Textual content of the memory.
52    pub content: String,
53    /// Importance score in `[0.0, 1.0]`. Higher = more important.
54    pub importance: f32,
55    /// UTC timestamp when this memory was recorded.
56    pub timestamp: DateTime<Utc>,
57    /// Searchable tags attached to this memory.
58    pub tags: Vec<String>,
59    /// Number of times this memory has been recalled. Updated in-place by `recall`.
60    #[serde(default)]
61    pub recall_count: u64,
62}
63
64impl MemoryItem {
65    /// Construct a new `MemoryItem` with the current timestamp and a random ID.
66    pub fn new(
67        agent_id: AgentId,
68        content: impl Into<String>,
69        importance: f32,
70        tags: Vec<String>,
71    ) -> Self {
72        Self {
73            id: MemoryId::random(),
74            agent_id,
75            content: content.into(),
76            importance: importance.clamp(0.0, 1.0),
77            timestamp: Utc::now(),
78            tags,
79            recall_count: 0,
80        }
81    }
82
83    /// Return the age of this memory in fractional hours since it was recorded.
84    ///
85    /// Returns `0.0` if the current time is somehow before `timestamp`.
86    pub fn age_hours(&self) -> f64 {
87        let now = Utc::now();
88        let elapsed = now.signed_duration_since(self.timestamp);
89        elapsed.num_milliseconds().max(0) as f64 / 3_600_000.0
90    }
91
92    /// Return `true` if this memory item has the given tag.
93    pub fn has_tag(&self, tag: &str) -> bool {
94        self.tags.iter().any(|t| t == tag)
95    }
96
97    /// Return the approximate number of whitespace-separated words in the content.
98    pub fn word_count(&self) -> usize {
99        self.content.split_whitespace().count()
100    }
101
102    /// Return the byte length of the content string.
103    pub fn content_len(&self) -> usize {
104        self.content.len()
105    }
106
107    /// Return the number of tags attached to this memory item.
108    pub fn tag_count(&self) -> usize {
109        self.tags.len()
110    }
111
112    /// Add `tag` to this memory item if it is not already present.
113    ///
114    /// Returns `true` if the tag was newly added, `false` if it was already present.
115    pub fn add_tag(&mut self, tag: impl Into<String>) -> bool {
116        let tag = tag.into();
117        if self.tags.iter().any(|t| t == &tag) {
118            return false;
119        }
120        self.tags.push(tag);
121        true
122    }
123
124    /// Remove `tag` from this memory item.
125    ///
126    /// Returns `true` if the tag was found and removed, `false` if it was not present.
127    pub fn remove_tag(&mut self, tag: &str) -> bool {
128        if let Some(pos) = self.tags.iter().position(|t| t == tag) {
129            self.tags.swap_remove(pos);
130            true
131        } else {
132            false
133        }
134    }
135
136    /// Return `true` if this item's importance score is strictly above `threshold`.
137    ///
138    /// Useful for filtering a collection to retain only high-value memories:
139    /// ```rust,ignore
140    /// let important: Vec<_> = items.iter().filter(|m| m.is_high_importance(0.7)).collect();
141    /// ```
142    pub fn is_high_importance(&self, threshold: f32) -> bool {
143        self.importance > threshold
144    }
145}
146
147impl std::fmt::Display for MemoryItem {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        write!(
150            f,
151            "[{}] importance={:.2} recalls={} content=\"{}\"",
152            self.id,
153            self.importance,
154            self.recall_count,
155            self.content
156        )
157    }
158}
159
160// ── DecayPolicy ───────────────────────────────────────────────────────────────
161
162/// Governs how memory importance decays over time.
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct DecayPolicy {
165    /// The half-life duration in hours. After this many hours, importance is halved.
166    half_life_hours: f64,
167}
168
169impl DecayPolicy {
170    /// Create an exponential decay policy with the given half-life in hours.
171    ///
172    /// # Arguments
173    /// * `half_life_hours` — time after which importance is halved; must be > 0
174    ///
175    /// # Returns
176    /// - `Ok(DecayPolicy)` — on success
177    /// - `Err(AgentRuntimeError::Memory)` — if `half_life_hours <= 0`
178    pub fn exponential(half_life_hours: f64) -> Result<Self, AgentRuntimeError> {
179        if half_life_hours <= 0.0 {
180            return Err(AgentRuntimeError::Memory(
181                "half_life_hours must be positive".into(),
182            ));
183        }
184        Ok(Self { half_life_hours })
185    }
186
187    /// Apply decay to an importance score based on elapsed time.
188    ///
189    /// # Arguments
190    /// * `importance` — original importance in `[0.0, 1.0]`
191    /// * `age_hours` — how many hours have passed since the memory was recorded
192    ///
193    /// # Returns
194    /// Decayed importance clamped to `[0.0, 1.0]`.
195    pub fn apply(&self, importance: f32, age_hours: f64) -> f32 {
196        let decay = (-age_hours * std::f64::consts::LN_2 / self.half_life_hours).exp();
197        (importance as f64 * decay).clamp(0.0, 1.0) as f32
198    }
199
200    /// Return the configured half-life in hours.
201    pub fn half_life_hours(&self) -> f64 {
202        self.half_life_hours
203    }
204
205    /// Apply decay in-place to a mutable `MemoryItem`.
206    pub fn decay_item(&self, item: &mut MemoryItem) {
207        let age_hours = (Utc::now() - item.timestamp).num_seconds().max(0) as f64 / 3600.0;
208        item.importance = self.apply(item.importance, age_hours);
209    }
210}
211
212impl std::fmt::Display for DecayPolicy {
213    /// Render as `"Exponential(half_life=Xh)"`.
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        write!(f, "Exponential(half_life={:.1}h)", self.half_life_hours)
216    }
217}
218
219// ── RecallPolicy ──────────────────────────────────────────────────────────────
220
221/// Controls how memories are scored and ranked during recall.
222///
223/// # Interaction with `DecayPolicy`
224///
225/// When both a `DecayPolicy` and a `RecallPolicy` are configured, decay is
226/// applied **before** scoring.  This means that for `RecallPolicy::Importance`,
227/// an old high-importance memory may rank lower than a fresh low-importance
228/// memory after decay has reduced its score.
229///
230/// For `RecallPolicy::Hybrid`, the `recency_weight` term already captures
231/// temporal distance; combining it with a `DecayPolicy` therefore applies a
232/// *double* time penalty — set one or the other, not both, unless the double
233/// penalty is intentional.
234///
235/// ## Score Calculation Example
236///
237/// Given two memories, each with `importance = 0.5`:
238/// - Memory A: `recall_count = 0`, inserted 1 hour ago
239/// - Memory B: `recall_count = 10`, inserted 10 hours ago
240///
241/// With `recency_weight = 1.0` and `frequency_weight = 0.1`:
242/// - Score A = `0.5 + 1.0 × 1.0 + 0.1 × 0` = `1.5` (recency wins)
243/// - Score B = `0.5 + 1.0 × (−10.0) + 0.1 × 10` = `−8.5` (old → ranked lower)
244///
245/// Note: the recency term uses negative hours-since-creation so older items score lower.
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub enum RecallPolicy {
248    /// Rank purely by importance score (default).
249    Importance,
250    /// Hybrid score: blends importance, recency, and recall frequency.
251    ///
252    /// `score = importance + recency_score * recency_weight + frequency_score * frequency_weight`
253    /// where `recency_score = exp(-age_hours / 24.0)` and
254    /// `frequency_score = recall_count / (max_recall_count + 1)` (normalized).
255    Hybrid {
256        /// Weight applied to the recency component of the hybrid score.
257        recency_weight: f32,
258        /// Weight applied to the recall-frequency component of the hybrid score.
259        frequency_weight: f32,
260    },
261}
262
263impl Default for RecallPolicy {
264    fn default() -> Self {
265        RecallPolicy::Importance
266    }
267}
268
269// ── Hybrid scoring helper ─────────────────────────────────────────────────────
270
271fn compute_hybrid_score(
272    item: &MemoryItem,
273    recency_weight: f32,
274    frequency_weight: f32,
275    max_recall: u64,
276    now: chrono::DateTime<Utc>,
277) -> f32 {
278    let age_hours = (now - item.timestamp).num_seconds().max(0) as f64 / 3600.0;
279    let recency_score = (-age_hours / 24.0).exp() as f32;
280    let frequency_score = item.recall_count as f32 / (max_recall as f32 + 1.0);
281    item.importance + recency_score * recency_weight + frequency_score * frequency_weight
282}
283
284// ── EvictionPolicy ────────────────────────────────────────────────────────────
285
286/// Policy controlling which item is evicted when the per-agent capacity is exceeded.
287#[derive(Debug, Clone, PartialEq, Eq, Default)]
288pub enum EvictionPolicy {
289    /// Evict the item with the lowest importance score (default).
290    #[default]
291    LowestImportance,
292    /// Evict the oldest item (by insertion order / timestamp).
293    Oldest,
294}
295
296// ── EpisodicStoreBuilder ──────────────────────────────────────────────────────
297
298/// Fluent builder for [`EpisodicStore`].
299///
300/// Allows combining any set of options — decay, recall policy, per-agent
301/// capacity, max age, and eviction policy — before creating the store.
302///
303/// # Example
304/// ```rust
305/// use llm_agent_runtime::memory::{EpisodicStore, EvictionPolicy, RecallPolicy, DecayPolicy};
306///
307/// let store = EpisodicStore::builder()
308///     .per_agent_capacity(50)
309///     .eviction_policy(EvictionPolicy::Oldest)
310///     .build();
311/// ```
312#[derive(Default)]
313pub struct EpisodicStoreBuilder {
314    decay: Option<DecayPolicy>,
315    recall_policy: Option<RecallPolicy>,
316    per_agent_capacity: Option<usize>,
317    max_age_hours: Option<f64>,
318    eviction_policy: Option<EvictionPolicy>,
319}
320
321impl EpisodicStoreBuilder {
322    /// Set the decay policy.
323    pub fn decay(mut self, policy: DecayPolicy) -> Self {
324        self.decay = Some(policy);
325        self
326    }
327
328    /// Set the recall policy.
329    pub fn recall_policy(mut self, policy: RecallPolicy) -> Self {
330        self.recall_policy = Some(policy);
331        self
332    }
333
334    /// Set the per-agent capacity. Panics if `capacity == 0`.
335    pub fn per_agent_capacity(mut self, capacity: usize) -> Self {
336        assert!(capacity > 0, "per_agent_capacity must be > 0");
337        self.per_agent_capacity = Some(capacity);
338        self
339    }
340
341    /// Set the per-agent capacity without panicking.
342    ///
343    /// Returns `Err` if `capacity == 0`. Prefer this over [`per_agent_capacity`]
344    /// in library/user-facing code where a misconfigured capacity should be
345    /// handled gracefully rather than aborting the process.
346    ///
347    /// [`per_agent_capacity`]: EpisodicStoreBuilder::per_agent_capacity
348    pub fn try_per_agent_capacity(
349        mut self,
350        capacity: usize,
351    ) -> Result<Self, crate::error::AgentRuntimeError> {
352        if capacity == 0 {
353            return Err(crate::error::AgentRuntimeError::Memory(
354                "per_agent_capacity must be > 0".into(),
355            ));
356        }
357        self.per_agent_capacity = Some(capacity);
358        Ok(self)
359    }
360
361    /// Set the maximum memory age in hours. Returns `Err` if `max_age_hours <= 0`.
362    pub fn max_age_hours(mut self, hours: f64) -> Result<Self, crate::error::AgentRuntimeError> {
363        if hours <= 0.0 {
364            return Err(crate::error::AgentRuntimeError::Memory(
365                "max_age_hours must be positive".into(),
366            ));
367        }
368        self.max_age_hours = Some(hours);
369        Ok(self)
370    }
371
372    /// Set the eviction policy.
373    pub fn eviction_policy(mut self, policy: EvictionPolicy) -> Self {
374        self.eviction_policy = Some(policy);
375        self
376    }
377
378    /// Consume the builder and create an [`EpisodicStore`].
379    pub fn build(self) -> EpisodicStore {
380        // Warn when both a DecayPolicy and RecallPolicy::Hybrid are active:
381        // decay is applied before scoring, so Hybrid's recency term produces a
382        // double time penalty.  Set one or the other unless the double penalty
383        // is intentional.
384        if self.decay.is_some() {
385            if let Some(RecallPolicy::Hybrid { .. }) = &self.recall_policy {
386                tracing::warn!(
387                    "EpisodicStore configured with both DecayPolicy and RecallPolicy::Hybrid \
388                     — time-based decay is applied before hybrid scoring, resulting in a \
389                     double time penalty.  Set one or the other unless this is intentional."
390                );
391            }
392        }
393        EpisodicStore {
394            inner: Arc::new(Mutex::new(EpisodicInner {
395                items: HashMap::new(),
396                decay: self.decay,
397                recall_policy: self.recall_policy.unwrap_or(RecallPolicy::Importance),
398                per_agent_capacity: self.per_agent_capacity,
399                max_age_hours: self.max_age_hours,
400                eviction_policy: self.eviction_policy.unwrap_or_default(),
401            })),
402        }
403    }
404}
405
406// ── EpisodicStore ─────────────────────────────────────────────────────────────
407
408/// Stores episodic (event-based) memories for agents, ordered by insertion time.
409///
410/// ## Guarantees
411/// - Thread-safe via `Arc<Mutex<_>>`
412/// - Ordered: recall returns items in descending importance order
413/// - Bounded by optional per-agent capacity
414/// - O(1) agent lookup via per-agent `HashMap` index
415/// - Automatic expiry via optional `max_age_hours`
416#[derive(Debug, Clone)]
417pub struct EpisodicStore {
418    inner: Arc<Mutex<EpisodicInner>>,
419}
420
421#[derive(Debug)]
422struct EpisodicInner {
423    /// Items stored per-agent for O(1) lookup. The key is the agent ID.
424    items: HashMap<AgentId, Vec<MemoryItem>>,
425    decay: Option<DecayPolicy>,
426    recall_policy: RecallPolicy,
427    /// Maximum items stored per agent. Oldest (lowest-importance) items evicted when exceeded.
428    per_agent_capacity: Option<usize>,
429    /// Maximum age in hours. Items older than this are purged on the next recall or add.
430    max_age_hours: Option<f64>,
431    /// Eviction policy when per_agent_capacity is exceeded.
432    eviction_policy: EvictionPolicy,
433}
434
435impl EpisodicInner {
436    /// Purge items for `agent_id` that exceed `max_age_hours`, if configured.
437    fn purge_stale(&mut self, agent_id: &AgentId) {
438        if let Some(max_age_h) = self.max_age_hours {
439            let cutoff = Utc::now()
440                - chrono::Duration::seconds((max_age_h * 3600.0) as i64);
441            if let Some(agent_items) = self.items.get_mut(agent_id) {
442                agent_items.retain(|i| i.timestamp >= cutoff);
443            }
444        }
445    }
446}
447
448/// Evict one item from `agent_items` if `len > cap`, according to `policy`.
449///
450/// The last element (the just-inserted item) is excluded from the
451/// `LowestImportance` scan so that newly added items are never evicted.
452fn evict_if_over_capacity(
453    agent_items: &mut Vec<MemoryItem>,
454    cap: usize,
455    policy: &EvictionPolicy,
456) {
457    if agent_items.len() <= cap {
458        return;
459    }
460    let pos = match policy {
461        EvictionPolicy::LowestImportance => {
462            let len = agent_items.len();
463            agent_items[..len - 1]
464                .iter()
465                .enumerate()
466                .min_by(|(_, a), (_, b)| {
467                    a.importance
468                        .partial_cmp(&b.importance)
469                        .unwrap_or(std::cmp::Ordering::Equal)
470                })
471                .map(|(pos, _)| pos)
472        }
473        EvictionPolicy::Oldest => {
474            let len = agent_items.len();
475            agent_items[..len - 1]
476                .iter()
477                .enumerate()
478                .min_by_key(|(_, item)| item.timestamp)
479                .map(|(pos, _)| pos)
480        }
481    };
482    if let Some(pos) = pos {
483        agent_items.remove(pos);
484    }
485}
486
487impl EpisodicStore {
488    /// Create a new unbounded episodic store without decay.
489    pub fn new() -> Self {
490        Self {
491            inner: Arc::new(Mutex::new(EpisodicInner {
492                items: HashMap::new(),
493                decay: None,
494                recall_policy: RecallPolicy::Importance,
495                per_agent_capacity: None,
496                max_age_hours: None,
497                eviction_policy: EvictionPolicy::LowestImportance,
498            })),
499        }
500    }
501
502    /// Return a fluent builder to construct an `EpisodicStore` with any combination of options.
503    pub fn builder() -> EpisodicStoreBuilder {
504        EpisodicStoreBuilder::default()
505    }
506
507    /// Create a new episodic store with the given decay policy.
508    pub fn with_decay(policy: DecayPolicy) -> Self {
509        Self {
510            inner: Arc::new(Mutex::new(EpisodicInner {
511                items: HashMap::new(),
512                decay: Some(policy),
513                recall_policy: RecallPolicy::Importance,
514                per_agent_capacity: None,
515                max_age_hours: None,
516                eviction_policy: EvictionPolicy::LowestImportance,
517            })),
518        }
519    }
520
521    /// Create a new episodic store with both a decay policy and a recall policy.
522    ///
523    /// # Warning
524    ///
525    /// When `recall` is [`RecallPolicy::Hybrid`], decay is applied **before**
526    /// scoring, producing a double time penalty.  See [`RecallPolicy`] docs for
527    /// details.  Consider using the [`builder`](EpisodicStore::builder) to
528    /// configure only one of these time-based mechanisms.
529    pub fn with_decay_and_recall_policy(decay: DecayPolicy, recall: RecallPolicy) -> Self {
530        if let RecallPolicy::Hybrid { .. } = &recall {
531            tracing::warn!(
532                "EpisodicStore::with_decay_and_recall_policy called with RecallPolicy::Hybrid \
533                 — this applies a double time penalty.  Set DecayPolicy OR Hybrid recency \
534                 weighting, not both, unless the double penalty is intentional."
535            );
536        }
537        Self {
538            inner: Arc::new(Mutex::new(EpisodicInner {
539                items: HashMap::new(),
540                decay: Some(decay),
541                recall_policy: recall,
542                per_agent_capacity: None,
543                max_age_hours: None,
544                eviction_policy: EvictionPolicy::LowestImportance,
545            })),
546        }
547    }
548
549    /// Create a new episodic store with the given recall policy.
550    pub fn with_recall_policy(policy: RecallPolicy) -> Self {
551        Self {
552            inner: Arc::new(Mutex::new(EpisodicInner {
553                items: HashMap::new(),
554                decay: None,
555                recall_policy: policy,
556                per_agent_capacity: None,
557                max_age_hours: None,
558                eviction_policy: EvictionPolicy::LowestImportance,
559            })),
560        }
561    }
562
563    /// Create a new episodic store with the given per-agent capacity limit.
564    ///
565    /// When an agent exceeds this capacity, the lowest-importance item for that
566    /// agent is evicted.
567    ///
568    /// # Soft-limit semantics
569    ///
570    /// The capacity is a *soft* limit.  During each [`add_episode`] call the
571    /// new item is inserted first, and only then is the lowest-importance item
572    /// evicted if the count exceeds `capacity`.  This means the store
573    /// momentarily holds `capacity + 1` items per agent while eviction is in
574    /// progress.  The newly added item is **never** the one evicted regardless
575    /// of its importance score.
576    ///
577    /// Concurrent calls to `add_episode` may briefly exceed the cap by more
578    /// than one item before each call performs its own eviction sweep.
579    ///
580    /// [`add_episode`]: EpisodicStore::add_episode
581    pub fn with_per_agent_capacity(capacity: usize) -> Self {
582        assert!(capacity > 0, "per_agent_capacity must be > 0");
583        Self {
584            inner: Arc::new(Mutex::new(EpisodicInner {
585                items: HashMap::new(),
586                decay: None,
587                recall_policy: RecallPolicy::Importance,
588                per_agent_capacity: Some(capacity),
589                max_age_hours: None,
590                eviction_policy: EvictionPolicy::LowestImportance,
591            })),
592        }
593    }
594
595    /// Create a new episodic store with a per-agent capacity limit, without panicking.
596    ///
597    /// Returns `Err` if `capacity == 0`. Prefer this over [`with_per_agent_capacity`]
598    /// in user-facing code where a zero capacity should be a recoverable error
599    /// rather than a panic.
600    ///
601    /// [`with_per_agent_capacity`]: EpisodicStore::with_per_agent_capacity
602    pub fn try_with_per_agent_capacity(
603        capacity: usize,
604    ) -> Result<Self, AgentRuntimeError> {
605        if capacity == 0 {
606            return Err(AgentRuntimeError::Memory(
607                "per_agent_capacity must be > 0".into(),
608            ));
609        }
610        Ok(Self {
611            inner: Arc::new(Mutex::new(EpisodicInner {
612                items: HashMap::new(),
613                decay: None,
614                recall_policy: RecallPolicy::Importance,
615                per_agent_capacity: Some(capacity),
616                max_age_hours: None,
617                eviction_policy: EvictionPolicy::LowestImportance,
618            })),
619        })
620    }
621
622    /// Create a new episodic store with an absolute age limit.
623    ///
624    /// Items older than `max_age_hours` are automatically purged on the next
625    /// `recall` or `add_episode` call for the owning agent.
626    ///
627    /// # Arguments
628    /// * `max_age_hours` — maximum memory age in hours; must be > 0
629    pub fn with_max_age(max_age_hours: f64) -> Result<Self, AgentRuntimeError> {
630        if max_age_hours <= 0.0 {
631            return Err(AgentRuntimeError::Memory(
632                "max_age_hours must be positive".into(),
633            ));
634        }
635        Ok(Self {
636            inner: Arc::new(Mutex::new(EpisodicInner {
637                items: HashMap::new(),
638                decay: None,
639                recall_policy: RecallPolicy::Importance,
640                per_agent_capacity: None,
641                max_age_hours: Some(max_age_hours),
642                eviction_policy: EvictionPolicy::LowestImportance,
643            })),
644        })
645    }
646
647    /// Create a new episodic store with the given eviction policy.
648    pub fn with_eviction_policy(policy: EvictionPolicy) -> Self {
649        Self {
650            inner: Arc::new(Mutex::new(EpisodicInner {
651                items: HashMap::new(),
652                decay: None,
653                recall_policy: RecallPolicy::Importance,
654                per_agent_capacity: None,
655                max_age_hours: None,
656                eviction_policy: policy,
657            })),
658        }
659    }
660
661    /// Record a new episode for the given agent.
662    ///
663    /// # Returns
664    /// The `MemoryId` of the newly created memory item.
665    ///
666    /// # Errors
667    /// Returns `Err(AgentRuntimeError::Memory)` only if the internal mutex is
668    /// poisoned (extremely unlikely in normal operation; see [`recover_lock`]).
669    ///
670    /// # Capacity enforcement
671    ///
672    /// If the store was created with [`with_per_agent_capacity`], the item is
673    /// always inserted first.  If the agent's item count then exceeds the cap,
674    /// the single lowest-importance item for that agent is evicted.  See
675    /// [`with_per_agent_capacity`] for the full soft-limit semantics.
676    ///
677    /// [`with_per_agent_capacity`]: EpisodicStore::with_per_agent_capacity
678    #[tracing::instrument(skip(self))]
679    pub fn add_episode(
680        &self,
681        agent_id: AgentId,
682        content: impl Into<String> + std::fmt::Debug,
683        importance: f32,
684    ) -> Result<MemoryId, AgentRuntimeError> {
685        let item = MemoryItem::new(agent_id.clone(), content, importance, Vec::new());
686        let id = item.id.clone();
687        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::add_episode");
688
689        inner.purge_stale(&agent_id);
690        let cap = inner.per_agent_capacity; // read before mutable borrow
691        let eviction_policy = inner.eviction_policy.clone();
692        let agent_items = inner.items.entry(agent_id).or_default();
693        agent_items.push(item);
694
695        if let Some(cap) = cap {
696            evict_if_over_capacity(agent_items, cap, &eviction_policy);
697        }
698        Ok(id)
699    }
700
701    /// Add an episode with associated tags.
702    ///
703    /// Convenience wrapper around [`add_episode`] that accepts a tag list in the
704    /// same call.  Episode capacity eviction follows the same rules as `add_episode`.
705    ///
706    /// [`add_episode`]: EpisodicStore::add_episode
707    #[tracing::instrument(skip(self))]
708    pub fn add_episode_with_tags(
709        &self,
710        agent_id: AgentId,
711        content: impl Into<String> + std::fmt::Debug,
712        importance: f32,
713        tags: Vec<String>,
714    ) -> Result<MemoryId, AgentRuntimeError> {
715        let item = MemoryItem::new(agent_id.clone(), content, importance, tags);
716        let id = item.id.clone();
717        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::add_episode_with_tags");
718        inner.purge_stale(&agent_id);
719        let cap = inner.per_agent_capacity;
720        let eviction_policy = inner.eviction_policy.clone();
721        let agent_items = inner.items.entry(agent_id).or_default();
722        agent_items.push(item);
723        if let Some(cap) = cap {
724            evict_if_over_capacity(agent_items, cap, &eviction_policy);
725        }
726        Ok(id)
727    }
728
729    /// Remove a specific episode by its `MemoryId`.
730    ///
731    /// Returns `Ok(true)` if the episode was found and removed, `Ok(false)` if
732    /// no episode with that `id` exists for `agent_id`.
733    pub fn remove_by_id(
734        &self,
735        agent_id: &AgentId,
736        id: &MemoryId,
737    ) -> Result<bool, AgentRuntimeError> {
738        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::remove_by_id");
739        if let Some(items) = inner.items.get_mut(agent_id) {
740            if let Some(pos) = items.iter().position(|i| &i.id == id) {
741                items.remove(pos);
742                return Ok(true);
743            }
744        }
745        Ok(false)
746    }
747
748    /// Update the `tags` of an episode identified by its `MemoryId`.
749    ///
750    /// Returns `Ok(true)` if found and updated, `Ok(false)` otherwise.
751    pub fn update_tags_by_id(
752        &self,
753        agent_id: &AgentId,
754        id: &MemoryId,
755        new_tags: Vec<String>,
756    ) -> Result<bool, AgentRuntimeError> {
757        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::update_tags_by_id");
758        if let Some(items) = inner.items.get_mut(agent_id) {
759            if let Some(item) = items.iter_mut().find(|i| &i.id == id) {
760                item.tags = new_tags;
761                return Ok(true);
762            }
763        }
764        Ok(false)
765    }
766
767    /// Return the highest importance score across all episodes for `agent_id`.
768    ///
769    /// Returns `None` if the agent has no stored episodes.
770    pub fn max_importance_for(
771        &self,
772        agent_id: &AgentId,
773    ) -> Result<Option<f32>, AgentRuntimeError> {
774        let inner = recover_lock(self.inner.lock(), "EpisodicStore::max_importance_for");
775        let max = inner
776            .items
777            .get(agent_id)
778            .and_then(|items| {
779                items
780                    .iter()
781                    .map(|i| i.importance)
782                    .reduce(f32::max)
783            });
784        Ok(max)
785    }
786
787    /// Return the number of episodes stored for `agent_id`.
788    ///
789    /// Cheaper than `recall(agent, usize::MAX)?.len()` because it does not
790    /// sort, clone, or increment recall counts.
791    pub fn count_for(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
792        let inner = recover_lock(self.inner.lock(), "EpisodicStore::count_for");
793        Ok(inner.items.get(agent_id).map_or(0, |v| v.len()))
794    }
795
796    /// Return `true` if the store contains at least one episode for `agent_id`.
797    ///
798    /// Cheaper than `count_for(agent_id)? > 0` because no heap allocation occurs.
799    pub fn has_agent(&self, agent_id: &AgentId) -> Result<bool, AgentRuntimeError> {
800        let inner = recover_lock(self.inner.lock(), "EpisodicStore::has_agent");
801        Ok(inner.items.get(agent_id).map_or(false, |v| !v.is_empty()))
802    }
803
804    /// Return the IDs of agents that have at least `min` episodes.
805    pub fn agents_with_min_episodes(&self, min: usize) -> Result<Vec<AgentId>, AgentRuntimeError> {
806        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agents_with_min_episodes");
807        let mut ids: Vec<AgentId> = inner
808            .items
809            .iter()
810            .filter(|(_, v)| v.len() >= min)
811            .map(|(id, _)| id.clone())
812            .collect();
813        ids.sort_by(|a, b| a.0.cmp(&b.0));
814        Ok(ids)
815    }
816
817    /// Return the total number of episodes stored across all agents.
818    pub fn total_episode_count(&self) -> Result<usize, AgentRuntimeError> {
819        let inner = recover_lock(self.inner.lock(), "EpisodicStore::total_episode_count");
820        Ok(inner.items.values().map(|v| v.len()).sum())
821    }
822
823    /// Return all agent IDs that have at least one episode stored.
824    ///
825    /// The returned `Vec` is sorted for deterministic output.
826    pub fn agent_ids_with_episodes(&self) -> Result<Vec<AgentId>, AgentRuntimeError> {
827        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agent_ids_with_episodes");
828        let mut ids: Vec<AgentId> = inner
829            .items
830            .iter()
831            .filter(|(_, v)| !v.is_empty())
832            .map(|(k, _)| k.clone())
833            .collect();
834        ids.sort_by(|a, b| a.as_str().cmp(b.as_str()));
835        Ok(ids)
836    }
837
838    /// Return the average importance score of all episodes for `agent_id`.
839    ///
840    /// Returns `0.0` when the agent has no episodes or is unknown.
841    pub fn episode_importance_avg(&self, agent_id: &AgentId) -> Result<f64, AgentRuntimeError> {
842        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episode_importance_avg");
843        let items = match inner.items.get(agent_id) {
844            Some(v) if !v.is_empty() => v,
845            _ => return Ok(0.0),
846        };
847        let sum: f64 = items.iter().map(|m| m.importance as f64).sum();
848        Ok(sum / items.len() as f64)
849    }
850
851    /// Return a map from each agent ID to their episode count.
852    ///
853    /// Returns an empty map for an empty store.
854    pub fn agent_episode_counts(
855        &self,
856    ) -> Result<std::collections::HashMap<AgentId, usize>, AgentRuntimeError> {
857        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agent_episode_counts");
858        Ok(inner
859            .items
860            .iter()
861            .map(|(id, items)| (id.clone(), items.len()))
862            .collect())
863    }
864
865    /// Return all episodes for `agent_id` sorted by importance descending
866    /// (most important first).
867    ///
868    /// Returns an empty `Vec` for an unknown agent.
869    pub fn episodes_for_agent_sorted_by_importance(
870        &self,
871        agent_id: &AgentId,
872    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
873        let inner =
874            recover_lock(self.inner.lock(), "EpisodicStore::episodes_for_agent_sorted_by_importance");
875        let mut items: Vec<MemoryItem> = inner
876            .items
877            .get(agent_id)
878            .map(|v| v.iter().cloned().collect())
879            .unwrap_or_default();
880        items.sort_by(|a, b| {
881            b.importance
882                .partial_cmp(&a.importance)
883                .unwrap_or(std::cmp::Ordering::Equal)
884        });
885        Ok(items)
886    }
887
888    /// Return all episodes for `agent_id` whose timestamp is strictly after
889    /// `after`.
890    ///
891    /// Returns an empty `Vec` for an unknown agent or when no episode qualifies.
892    pub fn episodes_after_timestamp(
893        &self,
894        agent_id: &AgentId,
895        after: DateTime<Utc>,
896    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
897        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episodes_after_timestamp");
898        Ok(inner
899            .items
900            .get(agent_id)
901            .map(|v| v.iter().filter(|m| m.timestamp > after).cloned().collect())
902            .unwrap_or_default())
903    }
904
905    /// Return the agent ID with the lowest average episode importance, or
906    /// `None` for an empty store.
907    pub fn agent_with_min_importance_avg(&self) -> Result<Option<AgentId>, AgentRuntimeError> {
908        let inner =
909            recover_lock(self.inner.lock(), "EpisodicStore::agent_with_min_importance_avg");
910        Ok(inner
911            .items
912            .iter()
913            .filter(|(_, v)| !v.is_empty())
914            .map(|(id, items)| {
915                let avg = items.iter().map(|m| m.importance as f64).sum::<f64>()
916                    / items.len() as f64;
917                (id.clone(), avg)
918            })
919            .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
920            .map(|(id, _)| id))
921    }
922
923    /// Return all episodes for `agent_id` whose content contains `substr`.
924    ///
925    /// Returns an empty `Vec` for an unknown agent or when no episode matches.
926    pub fn episodes_matching_content(
927        &self,
928        agent_id: &AgentId,
929        substr: &str,
930    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
931        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episodes_matching_content");
932        Ok(inner
933            .items
934            .get(agent_id)
935            .map(|v| v.iter().filter(|m| m.content.contains(substr)).cloned().collect())
936            .unwrap_or_default())
937    }
938
939    /// Return the agent ID whose episodes have the highest total importance
940    /// score, or `None` if the store is empty.
941    pub fn top_agent_by_importance(&self) -> Result<Option<AgentId>, AgentRuntimeError> {
942        let inner = recover_lock(self.inner.lock(), "EpisodicStore::top_agent_by_importance");
943        Ok(inner
944            .items
945            .iter()
946            .map(|(id, items)| {
947                let total: f32 = items.iter().map(|m| m.importance).sum();
948                (id.clone(), total)
949            })
950            .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
951            .map(|(id, _)| id))
952    }
953
954    /// Return the episode with the highest importance score for `agent_id`,
955    /// or `None` when the agent has no episodes.
956    pub fn highest_importance_episode(
957        &self,
958        agent_id: &AgentId,
959    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
960        let inner = recover_lock(self.inner.lock(), "EpisodicStore::highest_importance_episode");
961        Ok(inner
962            .items
963            .get(agent_id)
964            .and_then(|v| {
965                v.iter().max_by(|a, b| {
966                    a.importance
967                        .partial_cmp(&b.importance)
968                        .unwrap_or(std::cmp::Ordering::Equal)
969                })
970            })
971            .cloned())
972    }
973
974    /// Return the average number of whitespace-delimited words per episode
975    /// content string for `agent_id`.
976    ///
977    /// Returns `0.0` when the agent has no episodes.
978    pub fn avg_episode_content_words(
979        &self,
980        agent_id: &AgentId,
981    ) -> Result<f64, AgentRuntimeError> {
982        let inner = recover_lock(self.inner.lock(), "EpisodicStore::avg_episode_content_words");
983        let items = match inner.items.get(agent_id) {
984            Some(v) if !v.is_empty() => v,
985            _ => return Ok(0.0),
986        };
987        let total: usize = items
988            .iter()
989            .map(|m| m.content.split_whitespace().count())
990            .sum();
991        Ok(total as f64 / items.len() as f64)
992    }
993
994    /// Return `true` if two or more episodes for `agent_id` share identical
995    /// content strings.
996    ///
997    /// Returns `false` for an unknown agent or one with fewer than 2 episodes.
998    pub fn has_duplicate_content(
999        &self,
1000        agent_id: &AgentId,
1001    ) -> Result<bool, AgentRuntimeError> {
1002        let inner = recover_lock(self.inner.lock(), "EpisodicStore::has_duplicate_content");
1003        let items = match inner.items.get(agent_id) {
1004            Some(v) => v,
1005            None => return Ok(false),
1006        };
1007        let mut seen = std::collections::HashSet::new();
1008        Ok(items.iter().any(|m| !seen.insert(m.content.as_str())))
1009    }
1010
1011    /// Return the number of episodes stored for the given agent.
1012    ///
1013    /// Returns `0` if the agent has no recorded episodes.
1014    pub fn episode_count_for(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
1015        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episode_count_for");
1016        Ok(inner.items.get(agent_id).map_or(0, |v| v.len()))
1017    }
1018
1019    /// Return all episodes for `agent_id` whose importance is strictly above
1020    /// `min_importance`, cloned into a new `Vec`.
1021    pub fn episodes_with_importance_above(
1022        &self,
1023        agent_id: &AgentId,
1024        min_importance: f32,
1025    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1026        let inner = recover_lock(
1027            self.inner.lock(),
1028            "EpisodicStore::episodes_with_importance_above",
1029        );
1030        Ok(inner
1031            .items
1032            .get(agent_id)
1033            .map(|v| v.iter().filter(|m| m.importance > min_importance).cloned().collect())
1034            .unwrap_or_default())
1035    }
1036
1037    /// Return the sum of importance values for all episodes belonging to
1038    /// `agent_id`.
1039    ///
1040    /// Returns `0.0` when the agent has no recorded episodes.
1041    pub fn agent_episode_importance_sum(
1042        &self,
1043        agent_id: &AgentId,
1044    ) -> Result<f64, AgentRuntimeError> {
1045        let inner = recover_lock(
1046            self.inner.lock(),
1047            "EpisodicStore::agent_episode_importance_sum",
1048        );
1049        Ok(inner
1050            .items
1051            .get(agent_id)
1052            .map(|v| v.iter().map(|m| m.importance as f64).sum())
1053            .unwrap_or(0.0))
1054    }
1055
1056    /// Return all episodes for `agent_id` whose importance is in the closed
1057    /// interval `[min_importance, max_importance]`.
1058    pub fn episodes_in_range(
1059        &self,
1060        agent_id: &AgentId,
1061        min_importance: f32,
1062        max_importance: f32,
1063    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1064        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episodes_in_range");
1065        Ok(inner
1066            .items
1067            .get(agent_id)
1068            .map(|v| {
1069                v.iter()
1070                    .filter(|m| m.importance >= min_importance && m.importance <= max_importance)
1071                    .cloned()
1072                    .collect()
1073            })
1074            .unwrap_or_default())
1075    }
1076
1077    /// Return `(min_importance, max_importance)` for all episodes of
1078    /// `agent_id`, or `None` if the agent has no recorded episodes.
1079    pub fn agent_importance_range(
1080        &self,
1081        agent_id: &AgentId,
1082    ) -> Result<Option<(f32, f32)>, AgentRuntimeError> {
1083        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agent_importance_range");
1084        Ok(inner.items.get(agent_id).and_then(|v| {
1085            if v.is_empty() {
1086                return None;
1087            }
1088            let min = v.iter().map(|m| m.importance).fold(f32::INFINITY, f32::min);
1089            let max = v.iter().map(|m| m.importance).fold(f32::NEG_INFINITY, f32::max);
1090            Some((min, max))
1091        }))
1092    }
1093
1094    /// Return the minimum importance across all episodes for `agent_id`, or
1095    /// `None` when the agent has no recorded episodes.
1096    pub fn episodes_min_importance(
1097        &self,
1098        agent_id: &AgentId,
1099    ) -> Result<Option<f32>, AgentRuntimeError> {
1100        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episodes_min_importance");
1101        Ok(inner.items.get(agent_id).and_then(|v| {
1102            v.iter()
1103                .map(|m| m.importance)
1104                .reduce(f32::min)
1105        }))
1106    }
1107
1108    /// Return the minimum word count among all episode content strings for
1109    /// `agent_id`.
1110    ///
1111    /// Returns `0` when the agent has no recorded episodes.
1112    pub fn episode_min_content_words(
1113        &self,
1114        agent_id: &AgentId,
1115    ) -> Result<usize, AgentRuntimeError> {
1116        let inner = recover_lock(
1117            self.inner.lock(),
1118            "EpisodicStore::episode_min_content_words",
1119        );
1120        Ok(inner
1121            .items
1122            .get(agent_id)
1123            .and_then(|v| v.iter().map(|m| m.content.split_whitespace().count()).min())
1124            .unwrap_or(0))
1125    }
1126
1127    /// Return the `AgentId` of the agent with the most stored episodes, or
1128    /// `None` if the store is empty.
1129    pub fn agent_with_most_episodes(&self) -> Result<Option<AgentId>, AgentRuntimeError> {
1130        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agent_with_most_episodes");
1131        Ok(inner
1132            .items
1133            .iter()
1134            .max_by_key(|(_, v)| v.len())
1135            .map(|(id, _)| id.clone()))
1136    }
1137
1138    /// Return the maximum word count among all episode content strings for
1139    /// `agent_id`.
1140    ///
1141    /// Returns `0` when the agent has no recorded episodes.
1142    pub fn episode_max_content_words(
1143        &self,
1144        agent_id: &AgentId,
1145    ) -> Result<usize, AgentRuntimeError> {
1146        let inner = recover_lock(
1147            self.inner.lock(),
1148            "EpisodicStore::episode_max_content_words",
1149        );
1150        Ok(inner
1151            .items
1152            .get(agent_id)
1153            .and_then(|v| v.iter().map(|m| m.content.split_whitespace().count()).max())
1154            .unwrap_or(0))
1155    }
1156
1157    /// Return the total number of episodes stored across all agents.
1158    pub fn total_items(&self) -> Result<usize, AgentRuntimeError> {
1159        let inner = recover_lock(self.inner.lock(), "EpisodicStore::total_items");
1160        Ok(inner.items.values().map(|v| v.len()).sum())
1161    }
1162
1163    /// Return a cloned `Vec` of all `MemoryItem`s across all agents, in an
1164    /// unspecified order.
1165    pub fn all_episodes(&self) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1166        let inner = recover_lock(self.inner.lock(), "EpisodicStore::all_episodes");
1167        Ok(inner.items.values().flat_map(|v| v.iter().cloned()).collect())
1168    }
1169
1170    /// Return all agent IDs that have at least one stored episode, sorted.
1171    pub fn agents(&self) -> Result<Vec<AgentId>, AgentRuntimeError> {
1172        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agents");
1173        let mut ids: Vec<AgentId> = inner
1174            .items
1175            .keys()
1176            .filter(|id| !inner.items[id].is_empty())
1177            .cloned()
1178            .collect();
1179        ids.sort_unstable_by(|a, b| a.0.cmp(&b.0));
1180        Ok(ids)
1181    }
1182
1183    /// Return the agent with the fewest (non-zero) stored episodes, or `None`
1184    /// if the store is empty.
1185    pub fn min_episode_count(&self) -> Result<Option<(AgentId, usize)>, AgentRuntimeError> {
1186        let inner = recover_lock(self.inner.lock(), "EpisodicStore::min_episode_count");
1187        Ok(inner
1188            .items
1189            .iter()
1190            .filter(|(_, v)| !v.is_empty())
1191            .min_by_key(|(_, v)| v.len())
1192            .map(|(id, v)| (id.clone(), v.len())))
1193    }
1194
1195    /// Return the highest importance value across all stored episodes,
1196    /// or `None` if the store is empty.
1197    pub fn max_importance_overall(&self) -> Result<Option<f32>, AgentRuntimeError> {
1198        let inner = recover_lock(self.inner.lock(), "EpisodicStore::max_importance_overall");
1199        let max = inner
1200            .items
1201            .values()
1202            .flat_map(|v| v.iter())
1203            .map(|e| e.importance)
1204            .reduce(f32::max);
1205        Ok(max)
1206    }
1207
1208    /// Return the variance of importance scores for the given agent's episodes.
1209    ///
1210    /// Returns `0.0` when the agent has fewer than two episodes.
1211    pub fn importance_variance_for(&self, agent_id: &AgentId) -> Result<f64, AgentRuntimeError> {
1212        let inner = recover_lock(self.inner.lock(), "EpisodicStore::importance_variance_for");
1213        let vals: Vec<f64> = inner
1214            .items
1215            .get(agent_id)
1216            .map(|v| v.iter().map(|e| e.importance as f64).collect())
1217            .unwrap_or_default();
1218        if vals.len() < 2 {
1219            return Ok(0.0);
1220        }
1221        let mean = vals.iter().sum::<f64>() / vals.len() as f64;
1222        let variance = vals.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / vals.len() as f64;
1223        Ok(variance)
1224    }
1225
1226    /// Return up to `n` episodes for `agent_id` sorted by descending importance
1227    /// without incrementing recall counts or applying decay.
1228    ///
1229    /// Use this for read-only importance-ranked snapshots.
1230    pub fn recall_top_n(
1231        &self,
1232        agent_id: &AgentId,
1233        n: usize,
1234    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1235        let inner = recover_lock(self.inner.lock(), "EpisodicStore::recall_top_n");
1236        let mut items: Vec<MemoryItem> = inner
1237            .items
1238            .get(agent_id)
1239            .cloned()
1240            .unwrap_or_default();
1241        items.sort_unstable_by(|a, b| {
1242            b.importance
1243                .partial_cmp(&a.importance)
1244                .unwrap_or(std::cmp::Ordering::Equal)
1245        });
1246        items.truncate(n);
1247        Ok(items)
1248    }
1249
1250    /// Return all episodes for `agent_id` whose importance is within
1251    /// `[min_inclusive, max_inclusive]`, sorted by descending importance.
1252    pub fn filter_by_importance(
1253        &self,
1254        agent_id: &AgentId,
1255        min: f32,
1256        max: f32,
1257    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1258        let inner = recover_lock(self.inner.lock(), "EpisodicStore::filter_by_importance");
1259        let mut items: Vec<MemoryItem> = inner
1260            .items
1261            .get(agent_id)
1262            .map(|v| {
1263                v.iter()
1264                    .filter(|i| i.importance >= min && i.importance <= max)
1265                    .cloned()
1266                    .collect()
1267            })
1268            .unwrap_or_default();
1269        items.sort_unstable_by(|a, b| {
1270            b.importance
1271                .partial_cmp(&a.importance)
1272                .unwrap_or(std::cmp::Ordering::Equal)
1273        });
1274        Ok(items)
1275    }
1276
1277    /// Keep only the `n` most-important episodes for `agent_id`, removing
1278    /// the rest.  Returns the number of episodes that were removed.
1279    ///
1280    /// If the agent has `n` or fewer episodes already, nothing is removed.
1281    pub fn retain_top_n(&self, agent_id: &AgentId, n: usize) -> Result<usize, AgentRuntimeError> {
1282        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::retain_top_n");
1283        let items = inner.items.entry(agent_id.clone()).or_default();
1284        if items.len() <= n {
1285            return Ok(0);
1286        }
1287        items.sort_unstable_by(|a, b| {
1288            b.importance
1289                .partial_cmp(&a.importance)
1290                .unwrap_or(std::cmp::Ordering::Equal)
1291        });
1292        let removed = items.len() - n;
1293        items.truncate(n);
1294        Ok(removed)
1295    }
1296
1297    /// Return the most recently stored episode for `agent_id`, or `None` if
1298    /// the agent has no episodes.
1299    ///
1300    /// "Most recent" is defined as the last element in the stored list, which
1301    /// matches insertion order.
1302    pub fn most_recent(&self, agent_id: &AgentId) -> Result<Option<MemoryItem>, AgentRuntimeError> {
1303        let inner = recover_lock(self.inner.lock(), "EpisodicStore::most_recent");
1304        Ok(inner
1305            .items
1306            .get(agent_id)
1307            .and_then(|v| v.last().cloned()))
1308    }
1309
1310    /// Return the highest importance score for `agent_id`, or `None` if the
1311    /// agent has no episodes.
1312    pub fn max_importance(&self, agent_id: &AgentId) -> Result<Option<f32>, AgentRuntimeError> {
1313        let inner = recover_lock(self.inner.lock(), "EpisodicStore::max_importance");
1314        Ok(inner
1315            .items
1316            .get(agent_id)
1317            .and_then(|v| {
1318                v.iter()
1319                    .map(|i| i.importance)
1320                    .reduce(f32::max)
1321            }))
1322    }
1323
1324    /// Return the lowest importance score for `agent_id`, or `None` if the
1325    /// agent has no episodes.
1326    pub fn min_importance(&self, agent_id: &AgentId) -> Result<Option<f32>, AgentRuntimeError> {
1327        let inner = recover_lock(self.inner.lock(), "EpisodicStore::min_importance");
1328        Ok(inner
1329            .items
1330            .get(agent_id)
1331            .and_then(|v| {
1332                v.iter()
1333                    .map(|i| i.importance)
1334                    .reduce(f32::min)
1335            }))
1336    }
1337
1338    /// Count episodes for `agent_id` whose importance is strictly greater than
1339    /// `threshold`.
1340    ///
1341    /// Returns `0` if the agent has no episodes.
1342    pub fn count_above_importance(
1343        &self,
1344        agent_id: &AgentId,
1345        threshold: f32,
1346    ) -> Result<usize, AgentRuntimeError> {
1347        let inner = recover_lock(self.inner.lock(), "EpisodicStore::count_above_importance");
1348        Ok(inner
1349            .items
1350            .get(agent_id)
1351            .map(|v| v.iter().filter(|i| i.importance > threshold).count())
1352            .unwrap_or(0))
1353    }
1354
1355    /// Return the episode with the highest `recall_count` for `agent_id`.
1356    ///
1357    /// Returns `None` if the agent has no stored episodes.  When multiple
1358    /// episodes tie for the maximum recall count, any one of them may be returned.
1359    pub fn most_recalled(&self, agent_id: &AgentId) -> Result<Option<MemoryItem>, AgentRuntimeError> {
1360        let inner = recover_lock(self.inner.lock(), "EpisodicStore::most_recalled");
1361        Ok(inner
1362            .items
1363            .get(agent_id)
1364            .and_then(|v| v.iter().max_by_key(|i| i.recall_count))
1365            .cloned())
1366    }
1367
1368    /// Return the arithmetic mean importance for `agent_id`, or `0.0` if the
1369    /// agent has no stored episodes.
1370    pub fn importance_avg(&self, agent_id: &AgentId) -> Result<f32, AgentRuntimeError> {
1371        let inner = recover_lock(self.inner.lock(), "EpisodicStore::importance_avg");
1372        match inner.items.get(agent_id) {
1373            None => Ok(0.0),
1374            Some(items) if items.is_empty() => Ok(0.0),
1375            Some(items) => {
1376                let sum: f32 = items.iter().map(|i| i.importance).sum();
1377                Ok(sum / items.len() as f32)
1378            }
1379        }
1380    }
1381
1382    /// Remove duplicate episodes (same `content`) for `agent_id`, keeping only
1383    /// the episode with the highest importance for each distinct content string.
1384    ///
1385    /// Returns the number of episodes removed.
1386    pub fn deduplicate_content(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
1387        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::deduplicate_content");
1388        let Some(items) = inner.items.get_mut(agent_id) else {
1389            return Ok(0);
1390        };
1391        let before = items.len();
1392        // For each unique content keep the item with the highest importance.
1393        let mut seen: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
1394        let mut keepers: Vec<usize> = Vec::new();
1395        for (idx, item) in items.iter().enumerate() {
1396            match seen.get(&item.content) {
1397                None => {
1398                    seen.insert(item.content.clone(), keepers.len());
1399                    keepers.push(idx);
1400                }
1401                Some(&pos) => {
1402                    let kept_idx = keepers[pos];
1403                    if item.importance > items[kept_idx].importance {
1404                        keepers[pos] = idx;
1405                    }
1406                }
1407            }
1408        }
1409        let kept: std::collections::HashSet<usize> = keepers.into_iter().collect();
1410        let mut i = 0;
1411        items.retain(|_| {
1412            let keep = kept.contains(&i);
1413            i += 1;
1414            keep
1415        });
1416        Ok(before - items.len())
1417    }
1418
1419    /// Return all `AgentId`s that have at least one stored episode.
1420    pub fn agent_ids(&self) -> Result<Vec<AgentId>, AgentRuntimeError> {
1421        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agent_ids");
1422        Ok(inner.items.keys().cloned().collect())
1423    }
1424
1425    /// Return agent IDs that have more than `min_count` episodes stored.
1426    ///
1427    /// Useful for identifying high-activity agents in a shared store.
1428    pub fn agents_with_episodes_above_count(
1429        &self,
1430        min_count: usize,
1431    ) -> Result<Vec<AgentId>, AgentRuntimeError> {
1432        let inner = recover_lock(
1433            self.inner.lock(),
1434            "EpisodicStore::agents_with_episodes_above_count",
1435        );
1436        Ok(inner
1437            .items
1438            .iter()
1439            .filter(|(_, episodes)| episodes.len() > min_count)
1440            .map(|(id, _)| id.clone())
1441            .collect())
1442    }
1443
1444    /// Return all episodes for `agent_id` whose `content` contains `pattern`
1445    /// (case-sensitive substring match), sorted by descending importance.
1446    pub fn find_by_content(
1447        &self,
1448        agent_id: &AgentId,
1449        pattern: &str,
1450    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1451        let inner = recover_lock(self.inner.lock(), "EpisodicStore::find_by_content");
1452        let mut matches: Vec<MemoryItem> = inner
1453            .items
1454            .get(agent_id)
1455            .map(|items| {
1456                items
1457                    .iter()
1458                    .filter(|i| i.content.contains(pattern))
1459                    .cloned()
1460                    .collect()
1461            })
1462            .unwrap_or_default();
1463        matches.sort_unstable_by(|a, b| {
1464            b.importance
1465                .partial_cmp(&a.importance)
1466                .unwrap_or(std::cmp::Ordering::Equal)
1467        });
1468        Ok(matches)
1469    }
1470
1471    /// Remove all episodes stored for `agent_id`.
1472    ///
1473    /// Returns the number of episodes that were removed.
1474    pub fn clear_for(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
1475        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::clear_for");
1476        let count = inner.items.remove(agent_id).map_or(0, |v| v.len());
1477        Ok(count)
1478    }
1479
1480    /// Return the number of episodes for `agent_id` that carry the given tag.
1481    pub fn count_episodes_with_tag(
1482        &self,
1483        agent_id: &AgentId,
1484        tag: &str,
1485    ) -> Result<usize, AgentRuntimeError> {
1486        let inner = recover_lock(self.inner.lock(), "EpisodicStore::count_episodes_with_tag");
1487        let count = inner
1488            .items
1489            .get(agent_id)
1490            .map_or(0, |items| items.iter().filter(|i| i.has_tag(tag)).count());
1491        Ok(count)
1492    }
1493
1494    /// Return the content strings of all episodes for `agent_id` whose content contains `substring`.
1495    pub fn episodes_with_content(
1496        &self,
1497        agent_id: &AgentId,
1498        substring: &str,
1499    ) -> Result<Vec<String>, AgentRuntimeError> {
1500        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episodes_with_content");
1501        let items = inner
1502            .items
1503            .get(agent_id)
1504            .map_or_else(Vec::new, |v| {
1505                v.iter()
1506                    .filter(|i| i.content.contains(substring))
1507                    .map(|i| i.content.clone())
1508                    .collect()
1509            });
1510        Ok(items)
1511    }
1512
1513    /// Return the byte length of the longest episode content for `agent_id`.
1514    ///
1515    /// Returns `0` if the agent has no episodes.
1516    pub fn max_content_length(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
1517        let inner = recover_lock(self.inner.lock(), "EpisodicStore::max_content_length");
1518        Ok(inner
1519            .items
1520            .get(agent_id)
1521            .and_then(|v| v.iter().map(|i| i.content.len()).max())
1522            .unwrap_or(0))
1523    }
1524
1525    /// Return the byte length of the shortest episode content for `agent_id`.
1526    ///
1527    /// Returns `0` if the agent has no episodes.
1528    pub fn min_content_length(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
1529        let inner = recover_lock(self.inner.lock(), "EpisodicStore::min_content_length");
1530        Ok(inner
1531            .items
1532            .get(agent_id)
1533            .and_then(|v| v.iter().map(|i| i.content.len()).min())
1534            .unwrap_or(0))
1535    }
1536
1537    /// Return the content strings of all episodes for `agent_id`, sorted by
1538    /// descending importance (most important first).
1539    pub fn episodes_by_importance(
1540        &self,
1541        agent_id: &AgentId,
1542    ) -> Result<Vec<String>, AgentRuntimeError> {
1543        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episodes_by_importance");
1544        let mut items: Vec<(f32, String)> = inner
1545            .items
1546            .get(agent_id)
1547            .map_or_else(Vec::new, |v| {
1548                v.iter().map(|i| (i.importance, i.content.clone())).collect()
1549            });
1550        items.sort_unstable_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
1551        Ok(items.into_iter().map(|(_, c)| c).collect())
1552    }
1553
1554    /// Return the count of episodes for `agent_id` whose content contains `substring`.
1555    ///
1556    /// The match is case-sensitive.  Returns `0` if the agent has no episodes.
1557    pub fn content_contains_count(
1558        &self,
1559        agent_id: &AgentId,
1560        substring: &str,
1561    ) -> Result<usize, AgentRuntimeError> {
1562        let inner = recover_lock(self.inner.lock(), "EpisodicStore::content_contains_count");
1563        Ok(inner
1564            .items
1565            .get(agent_id)
1566            .map_or(0, |v| v.iter().filter(|i| i.content.contains(substring)).count()))
1567    }
1568
1569    /// Return a sorted list of all `AgentId`s that have at least one episode.
1570    pub fn agents_with_episodes(&self) -> Result<Vec<AgentId>, AgentRuntimeError> {
1571        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agents_with_episodes");
1572        let mut ids: Vec<AgentId> = inner
1573            .items
1574            .iter()
1575            .filter(|(_, v)| !v.is_empty())
1576            .map(|(k, _)| k.clone())
1577            .collect();
1578        ids.sort_unstable_by(|a, b| a.as_str().cmp(b.as_str()));
1579        Ok(ids)
1580    }
1581
1582    /// Return the total number of episodes stored across **all** agents.
1583    ///
1584    /// Equivalent to summing `episode_count_for` over every agent, but in a
1585    /// single lock acquisition.
1586    pub fn episode_count_all_agents(&self) -> Result<usize, AgentRuntimeError> {
1587        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episode_count_all_agents");
1588        Ok(inner.items.values().map(|v| v.len()).sum())
1589    }
1590
1591    /// Return the count of episodes whose importance is strictly greater than `threshold`.
1592    pub fn high_importance_count(
1593        &self,
1594        agent_id: &AgentId,
1595        threshold: f32,
1596    ) -> Result<usize, AgentRuntimeError> {
1597        let inner = recover_lock(self.inner.lock(), "EpisodicStore::high_importance_count");
1598        Ok(inner
1599            .items
1600            .get(agent_id)
1601            .map_or(0, |v| v.iter().filter(|i| i.importance > threshold).count()))
1602    }
1603
1604    /// Return a list of content byte lengths for all episodes belonging to `agent_id`, in storage order.
1605    pub fn content_lengths(&self, agent_id: &AgentId) -> Result<Vec<usize>, AgentRuntimeError> {
1606        let inner = recover_lock(self.inner.lock(), "EpisodicStore::content_lengths");
1607        let lengths = inner
1608            .items
1609            .get(agent_id)
1610            .map_or_else(Vec::new, |v| v.iter().map(|i| i.content.len()).collect());
1611        Ok(lengths)
1612    }
1613
1614    /// Return the total byte length of all episode content strings for `agent_id`.
1615    ///
1616    /// Returns `0` if the agent has no stored episodes.
1617    pub fn total_content_bytes(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
1618        let inner = recover_lock(self.inner.lock(), "EpisodicStore::total_content_bytes");
1619        let total = inner
1620            .items
1621            .get(agent_id)
1622            .map_or(0, |items| items.iter().map(|i| i.content.len()).sum());
1623        Ok(total)
1624    }
1625
1626    /// Return the average byte length of episode content strings for `agent_id`.
1627    ///
1628    /// Returns `0.0` if the agent has no stored episodes.
1629    pub fn avg_content_length(&self, agent_id: &AgentId) -> Result<f64, AgentRuntimeError> {
1630        let inner = recover_lock(self.inner.lock(), "EpisodicStore::avg_content_length");
1631        let items = match inner.items.get(agent_id) {
1632            Some(v) if !v.is_empty() => v,
1633            _ => return Ok(0.0),
1634        };
1635        let total: usize = items.iter().map(|i| i.content.len()).sum();
1636        Ok(total as f64 / items.len() as f64)
1637    }
1638
1639    /// Return the sum of all importance scores for `agent_id`.
1640    ///
1641    /// Returns `0.0` if the agent has no stored episodes.
1642    pub fn importance_sum(&self, agent_id: &AgentId) -> Result<f32, AgentRuntimeError> {
1643        let inner = recover_lock(self.inner.lock(), "EpisodicStore::importance_sum");
1644        let sum = inner
1645            .items
1646            .get(agent_id)
1647            .map_or(0.0, |items| items.iter().map(|i| i.importance).sum());
1648        Ok(sum)
1649    }
1650
1651    /// Recall up to `limit` episodes for `agent_id` that carry `tag`,
1652    /// sorted by descending importance.  `limit = 0` returns all matches.
1653    pub fn recall_by_tag(
1654        &self,
1655        agent_id: &AgentId,
1656        tag: &str,
1657        limit: usize,
1658    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1659        let inner = recover_lock(self.inner.lock(), "EpisodicStore::recall_by_tag");
1660        let mut matches: Vec<MemoryItem> = inner
1661            .items
1662            .get(agent_id)
1663            .map(|items| {
1664                items
1665                    .iter()
1666                    .filter(|i| i.tags.iter().any(|t| t == tag))
1667                    .cloned()
1668                    .collect()
1669            })
1670            .unwrap_or_default();
1671        matches.sort_unstable_by(|a, b| {
1672            b.importance
1673                .partial_cmp(&a.importance)
1674                .unwrap_or(std::cmp::Ordering::Equal)
1675        });
1676        if limit > 0 {
1677            matches.truncate(limit);
1678        }
1679        Ok(matches)
1680    }
1681
1682    /// Add an episode with an explicit timestamp.
1683    #[tracing::instrument(skip(self))]
1684    pub fn add_episode_at(
1685        &self,
1686        agent_id: AgentId,
1687        content: impl Into<String> + std::fmt::Debug,
1688        importance: f32,
1689        timestamp: chrono::DateTime<chrono::Utc>,
1690    ) -> Result<MemoryId, AgentRuntimeError> {
1691        let mut item = MemoryItem::new(agent_id.clone(), content, importance, Vec::new());
1692        item.timestamp = timestamp;
1693        let id = item.id.clone();
1694        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::add_episode_at");
1695
1696        inner.purge_stale(&agent_id);
1697        let cap = inner.per_agent_capacity; // read before mutable borrow
1698        let eviction_policy = inner.eviction_policy.clone();
1699        let agent_items = inner.items.entry(agent_id).or_default();
1700        agent_items.push(item);
1701
1702        if let Some(cap) = cap {
1703            evict_if_over_capacity(agent_items, cap, &eviction_policy);
1704        }
1705        Ok(id)
1706    }
1707
1708    /// Add multiple episodes for the same agent in a single lock acquisition.
1709    ///
1710    /// More efficient than calling [`add_episode`] in a loop when inserting
1711    /// many items at once.  Returns the generated [`MemoryId`]s in the same
1712    /// order as `episodes`.
1713    ///
1714    /// [`add_episode`]: EpisodicStore::add_episode
1715    pub fn add_episodes_batch(
1716        &self,
1717        agent_id: AgentId,
1718        episodes: impl IntoIterator<Item = (impl Into<String>, f32)>,
1719    ) -> Result<Vec<MemoryId>, AgentRuntimeError> {
1720        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::add_episodes_batch");
1721        inner.purge_stale(&agent_id);
1722        let cap = inner.per_agent_capacity;
1723        let eviction_policy = inner.eviction_policy.clone();
1724        let agent_items = inner.items.entry(agent_id.clone()).or_default();
1725
1726        let mut ids = Vec::new();
1727        for (content, importance) in episodes {
1728            let item = MemoryItem::new(agent_id.clone(), content, importance, Vec::new());
1729            ids.push(item.id.clone());
1730            agent_items.push(item);
1731        }
1732        if let Some(cap) = cap {
1733            evict_if_over_capacity(agent_items, cap, &eviction_policy);
1734        }
1735        Ok(ids)
1736    }
1737
1738    /// Recall up to `limit` memories for the given agent.
1739    ///
1740    /// Applies decay if configured, purges stale items if `max_age` is set,
1741    /// increments `recall_count` for each recalled item, then returns items
1742    /// sorted according to the configured `RecallPolicy`.
1743    ///
1744    /// # Errors
1745    /// Returns `Err(AgentRuntimeError::Memory)` only if the internal mutex is
1746    /// poisoned (extremely unlikely in normal operation).
1747    #[tracing::instrument(skip(self))]
1748    pub fn recall(
1749        &self,
1750        agent_id: &AgentId,
1751        limit: usize,
1752    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1753        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::recall");
1754
1755        // Clone policy values to avoid borrow conflicts.
1756        let decay = inner.decay.clone();
1757        let max_age = inner.max_age_hours;
1758        let recall_policy = inner.recall_policy.clone();
1759
1760        // Use get_mut to avoid creating ghost entries for unknown agents.
1761        if !inner.items.contains_key(agent_id) {
1762            return Ok(Vec::new());
1763        }
1764        let agent_items = inner.items.get_mut(agent_id).unwrap();
1765
1766        // Apply decay in-place.
1767        if let Some(ref policy) = decay {
1768            for item in agent_items.iter_mut() {
1769                policy.decay_item(item);
1770            }
1771        }
1772
1773        // Purge stale items.
1774        if let Some(max_age_h) = max_age {
1775            let cutoff =
1776                Utc::now() - chrono::Duration::seconds((max_age_h * 3600.0) as i64);
1777            agent_items.retain(|i| i.timestamp >= cutoff);
1778        }
1779
1780        // Build a sorted index list (descending by score) without cloning all items first.
1781        //
1782        // When `limit < indices.len()` we use a two-phase partial sort:
1783        //   1. `select_nth_unstable_by(limit - 1, cmp)` — O(n) — partitions the
1784        //      slice so that indices[0..limit] are the top-limit elements (unordered)
1785        //      and indices[limit..] are the remaining (discarded) elements.
1786        //   2. Sort only indices[0..limit] — O(limit log limit).
1787        // Total: O(n + limit log limit) vs O(n log n) for a full sort.
1788        let mut indices: Vec<usize> = (0..agent_items.len()).collect();
1789
1790        match recall_policy {
1791            RecallPolicy::Importance => {
1792                let cmp = |&a: &usize, &b: &usize| {
1793                    agent_items[b]
1794                        .importance
1795                        .partial_cmp(&agent_items[a].importance)
1796                        .unwrap_or(std::cmp::Ordering::Equal)
1797                };
1798                if limit > 0 && limit < indices.len() {
1799                    indices.select_nth_unstable_by(limit - 1, cmp);
1800                    indices[..limit].sort_unstable_by(cmp);
1801                } else {
1802                    indices.sort_unstable_by(cmp);
1803                }
1804            }
1805            RecallPolicy::Hybrid {
1806                recency_weight,
1807                frequency_weight,
1808            } => {
1809                let max_recall = agent_items
1810                    .iter()
1811                    .map(|i| i.recall_count)
1812                    .max()
1813                    .unwrap_or(1)
1814                    .max(1);
1815                let now = Utc::now();
1816                let cmp = |&a: &usize, &b: &usize| {
1817                    let score_a = compute_hybrid_score(
1818                        &agent_items[a],
1819                        recency_weight,
1820                        frequency_weight,
1821                        max_recall,
1822                        now,
1823                    );
1824                    let score_b = compute_hybrid_score(
1825                        &agent_items[b],
1826                        recency_weight,
1827                        frequency_weight,
1828                        max_recall,
1829                        now,
1830                    );
1831                    score_b
1832                        .partial_cmp(&score_a)
1833                        .unwrap_or(std::cmp::Ordering::Equal)
1834                };
1835                if limit > 0 && limit < indices.len() {
1836                    indices.select_nth_unstable_by(limit - 1, cmp);
1837                    indices[..limit].sort_unstable_by(cmp);
1838                } else {
1839                    indices.sort_unstable_by(cmp);
1840                }
1841            }
1842        }
1843
1844        indices.truncate(limit);
1845
1846        // Increment recall_count only for the surviving items.
1847        for &idx in &indices {
1848            agent_items[idx].recall_count += 1;
1849        }
1850
1851        // Clone only the surviving items, with already-incremented counts.
1852        let items: Vec<MemoryItem> = indices.iter().map(|&idx| agent_items[idx].clone()).collect();
1853
1854        tracing::debug!("recalled {} items", items.len());
1855        Ok(items)
1856    }
1857
1858    /// Recall episodes for `agent_id` that contain **all** of the specified `tags`.
1859    ///
1860    /// Returns at most `limit` items ordered by descending importance, consistent
1861    /// with [`recall`].  Pass an empty `tags` slice to match all episodes.
1862    ///
1863    /// [`recall`]: EpisodicStore::recall
1864    pub fn recall_tagged(
1865        &self,
1866        agent_id: &AgentId,
1867        tags: &[&str],
1868        limit: usize,
1869    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1870        let inner = recover_lock(self.inner.lock(), "EpisodicStore::recall_tagged");
1871        let items = inner.items.get(agent_id).cloned().unwrap_or_default();
1872        drop(inner);
1873        let mut matched: Vec<MemoryItem> = items
1874            .into_iter()
1875            .filter(|item| {
1876                tags.iter()
1877                    .all(|t| item.tags.iter().any(|it| it.as_str() == *t))
1878            })
1879            .collect();
1880        matched.sort_unstable_by(|a, b| b.importance.partial_cmp(&a.importance).unwrap_or(std::cmp::Ordering::Equal));
1881        if limit > 0 {
1882            matched.truncate(limit);
1883        }
1884        Ok(matched)
1885    }
1886
1887    /// Retrieve a single episode by its `MemoryId`.
1888    ///
1889    /// Returns `Ok(Some(item))` if found, `Ok(None)` if no episode with that ID
1890    /// exists for `agent_id`.
1891    pub fn recall_by_id(
1892        &self,
1893        agent_id: &AgentId,
1894        id: &MemoryId,
1895    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
1896        let inner = recover_lock(self.inner.lock(), "EpisodicStore::recall_by_id");
1897        Ok(inner
1898            .items
1899            .get(agent_id)
1900            .and_then(|items| items.iter().find(|i| &i.id == id).cloned()))
1901    }
1902
1903    /// Import all episodes from `other` for `agent_id` into this store.
1904    ///
1905    /// Episodes are appended without deduplication.  Capacity eviction is applied
1906    /// per-episode exactly as in [`add_episode`].
1907    ///
1908    /// [`add_episode`]: EpisodicStore::add_episode
1909    pub fn merge_from(
1910        &self,
1911        other: &EpisodicStore,
1912        agent_id: &AgentId,
1913    ) -> Result<usize, AgentRuntimeError> {
1914        let other_items = {
1915            let inner = recover_lock(other.inner.lock(), "EpisodicStore::merge_from:read");
1916            inner.items.get(agent_id).cloned().unwrap_or_default()
1917        };
1918        let count = other_items.len();
1919        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::merge_from:write");
1920        inner.purge_stale(agent_id);
1921        let cap = inner.per_agent_capacity;
1922        let bucket = inner.items.entry(agent_id.clone()).or_default();
1923        for item in other_items {
1924            if let Some(cap) = cap {
1925                while bucket.len() >= cap {
1926                    bucket.remove(0);
1927                }
1928            }
1929            bucket.push(item);
1930        }
1931        Ok(count)
1932    }
1933
1934    /// Update the importance score of a specific episode in-place.
1935    ///
1936    /// Returns `Ok(true)` if the episode was found and updated, `Ok(false)` if no
1937    /// episode with that `id` exists for `agent_id`.
1938    ///
1939    /// `new_importance` is clamped to `[0.0, 1.0]`.
1940    pub fn update_importance(
1941        &self,
1942        agent_id: &AgentId,
1943        id: &MemoryId,
1944        new_importance: f32,
1945    ) -> Result<bool, AgentRuntimeError> {
1946        let importance = new_importance.clamp(0.0, 1.0);
1947        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::update_importance");
1948        if let Some(items) = inner.items.get_mut(agent_id) {
1949            if let Some(item) = items.iter_mut().find(|i| &i.id == id) {
1950                item.importance = importance;
1951                return Ok(true);
1952            }
1953        }
1954        Ok(false)
1955    }
1956
1957    /// Recall episodes for `agent_id` inserted at or after `cutoff`.
1958    ///
1959    /// Returns items ordered by descending importance (same as [`recall`]).
1960    /// Pass `limit = 0` to return all matching items.
1961    ///
1962    /// [`recall`]: EpisodicStore::recall
1963    pub fn recall_since(
1964        &self,
1965        agent_id: &AgentId,
1966        cutoff: chrono::DateTime<chrono::Utc>,
1967        limit: usize,
1968    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1969        let inner = recover_lock(self.inner.lock(), "EpisodicStore::recall_since");
1970        let mut items: Vec<MemoryItem> = inner
1971            .items
1972            .get(agent_id)
1973            .cloned()
1974            .unwrap_or_default()
1975            .into_iter()
1976            .filter(|i| i.timestamp >= cutoff)
1977            .collect();
1978        drop(inner);
1979        items.sort_unstable_by(|a, b| {
1980            b.importance
1981                .partial_cmp(&a.importance)
1982                .unwrap_or(std::cmp::Ordering::Equal)
1983        });
1984        if limit > 0 {
1985            items.truncate(limit);
1986        }
1987        Ok(items)
1988    }
1989
1990    /// Update the `content` of an episode identified by its `MemoryId`.
1991    ///
1992    /// Returns `Ok(true)` if found and updated, `Ok(false)` if no episode with that
1993    /// `id` exists for `agent_id`.
1994    pub fn update_content(
1995        &self,
1996        agent_id: &AgentId,
1997        id: &MemoryId,
1998        new_content: impl Into<String>,
1999    ) -> Result<bool, AgentRuntimeError> {
2000        let new_content = new_content.into();
2001        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::update_content");
2002        if let Some(items) = inner.items.get_mut(agent_id) {
2003            if let Some(item) = items.iter_mut().find(|i| &i.id == id) {
2004                item.content = new_content;
2005                return Ok(true);
2006            }
2007        }
2008        Ok(false)
2009    }
2010
2011    /// Recall the most recently added episodes for `agent_id` in reverse insertion order.
2012    ///
2013    /// Unlike [`recall`] (which ranks by importance), this returns the `limit` most
2014    /// recently inserted items with no re-ordering.  Useful when recency matters more
2015    /// than importance, e.g. retrieving the latest context window entries.
2016    ///
2017    /// [`recall`]: EpisodicStore::recall
2018    pub fn recall_recent(
2019        &self,
2020        agent_id: &AgentId,
2021        limit: usize,
2022    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
2023        let inner = recover_lock(self.inner.lock(), "EpisodicStore::recall_recent");
2024        let items = inner.items.get(agent_id).cloned().unwrap_or_default();
2025        drop(inner);
2026        let start = if limit > 0 && limit < items.len() {
2027            items.len() - limit
2028        } else {
2029            0
2030        };
2031        Ok(items[start..].iter().rev().cloned().collect())
2032    }
2033
2034    /// Retrieve all stored episodes for `agent_id` without any limit.
2035    ///
2036    /// Returns items in descending importance order, consistent with [`recall`].
2037    /// For large stores, prefer [`recall`] with an explicit limit.
2038    ///
2039    /// [`recall`]: EpisodicStore::recall
2040    pub fn recall_all(&self, agent_id: &AgentId) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
2041        self.recall(agent_id, usize::MAX)
2042    }
2043
2044    /// Return the top `n` episodes for `agent_id` ordered by descending importance.
2045    ///
2046    /// When `n == 0` all episodes are returned. Does not increment `recall_count`.
2047    pub fn top_n(&self, agent_id: &AgentId, n: usize) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
2048        let inner = recover_lock(self.inner.lock(), "EpisodicStore::top_n");
2049        let mut items = inner.items.get(agent_id).cloned().unwrap_or_default();
2050        items.sort_unstable_by(|a, b| b.importance.partial_cmp(&a.importance).unwrap_or(std::cmp::Ordering::Equal));
2051        if n > 0 {
2052            items.truncate(n);
2053        }
2054        Ok(items)
2055    }
2056
2057    /// Return episodes for `agent_id` whose importance is in `[min, max]`, most
2058    /// important first. Passing `limit == 0` returns all matching episodes.
2059    pub fn search_by_importance_range(
2060        &self,
2061        agent_id: &AgentId,
2062        min: f32,
2063        max: f32,
2064        limit: usize,
2065    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
2066        let inner = recover_lock(self.inner.lock(), "EpisodicStore::search_by_importance_range");
2067        let mut items: Vec<MemoryItem> = inner
2068            .items
2069            .get(agent_id)
2070            .map_or_else(Vec::new, |v| {
2071                v.iter().filter(|i| i.importance >= min && i.importance <= max).cloned().collect()
2072            });
2073        items.sort_unstable_by(|a, b| b.importance.partial_cmp(&a.importance).unwrap_or(std::cmp::Ordering::Equal));
2074        if limit > 0 {
2075            items.truncate(limit);
2076        }
2077        Ok(items)
2078    }
2079
2080    /// Return the sum of `recall_count` across all episodes for `agent_id`.
2081    ///
2082    /// Useful for tracking aggregate access frequency per agent.
2083    pub fn total_recall_count(&self, agent_id: &AgentId) -> Result<u64, AgentRuntimeError> {
2084        let inner = recover_lock(self.inner.lock(), "EpisodicStore::total_recall_count");
2085        Ok(inner
2086            .items
2087            .get(agent_id)
2088            .map_or(0, |items| items.iter().map(|i| i.recall_count).sum()))
2089    }
2090
2091    /// Return the `recall_count` of the episode identified by `id` for `agent_id`.
2092    ///
2093    /// Returns `None` if no episode with that `id` exists for the agent.
2094    pub fn recall_count_for(
2095        &self,
2096        agent_id: &AgentId,
2097        id: &MemoryId,
2098    ) -> Result<Option<u64>, AgentRuntimeError> {
2099        let inner = recover_lock(self.inner.lock(), "EpisodicStore::recall_count_for");
2100        Ok(inner
2101            .items
2102            .get(agent_id)
2103            .and_then(|items| items.iter().find(|i| &i.id == id))
2104            .map(|i| i.recall_count))
2105    }
2106
2107    /// Return summary statistics (count, min, max, mean importance) for `agent_id`.
2108    ///
2109    /// Returns `(0, 0.0, 0.0, 0.0)` if the agent has no stored episodes.
2110    pub fn importance_stats(&self, agent_id: &AgentId) -> Result<(usize, f32, f32, f32), AgentRuntimeError> {
2111        let inner = recover_lock(self.inner.lock(), "EpisodicStore::importance_stats");
2112        let items = inner.items.get(agent_id).map(|v| v.as_slice()).unwrap_or(&[]);
2113        if items.is_empty() {
2114            return Ok((0, 0.0, 0.0, 0.0));
2115        }
2116        let count = items.len();
2117        let min = items.iter().map(|i| i.importance).fold(f32::MAX, f32::min);
2118        let max = items.iter().map(|i| i.importance).fold(f32::MIN, f32::max);
2119        let mean = items.iter().map(|i| i.importance).sum::<f32>() / count as f32;
2120        Ok((count, min, max, mean))
2121    }
2122
2123    /// Return the oldest (first-inserted) episode for `agent_id`, or `None`
2124    /// if the agent has no stored episodes.
2125    pub fn oldest(
2126        &self,
2127        agent_id: &AgentId,
2128    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
2129        let inner = recover_lock(self.inner.lock(), "EpisodicStore::oldest");
2130        let item = inner.items.get(agent_id).and_then(|v| v.first()).cloned();
2131        Ok(item)
2132    }
2133
2134    /// Remove all stored episodes for `agent_id`.
2135    ///
2136    /// Returns the number of episodes that were removed.
2137    pub fn clear_agent(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
2138        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::clear_agent");
2139        let count = inner.items.remove(agent_id).map_or(0, |v| v.len());
2140        Ok(count)
2141    }
2142
2143    /// Return the episode with the earliest timestamp for `agent_id`, or `None`
2144    /// if the agent has no stored episodes.
2145    pub fn oldest_episode(
2146        &self,
2147        agent_id: &AgentId,
2148    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
2149        let inner = recover_lock(self.inner.lock(), "EpisodicStore::oldest_episode");
2150        Ok(inner
2151            .items
2152            .get(agent_id)
2153            .and_then(|v| v.iter().min_by_key(|i| i.timestamp))
2154            .cloned())
2155    }
2156
2157    /// Return the most recently stored episode for `agent_id` by timestamp, or
2158    /// `None` if the agent has no episodes.
2159    pub fn newest_episode(
2160        &self,
2161        agent_id: &AgentId,
2162    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
2163        let inner = recover_lock(self.inner.lock(), "EpisodicStore::newest_episode");
2164        Ok(inner
2165            .items
2166            .get(agent_id)
2167            .and_then(|v| v.iter().max_by_key(|i| i.timestamp))
2168            .cloned())
2169    }
2170
2171    /// Return the `n` most recently stored episodes for `agent_id`, sorted by
2172    /// timestamp descending (newest first).
2173    ///
2174    /// Returns fewer than `n` items when the agent has fewer stored episodes.
2175    /// Returns an empty `Vec` for an unknown agent.
2176    pub fn recent_episodes(
2177        &self,
2178        agent_id: &AgentId,
2179        n: usize,
2180    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
2181        let inner = recover_lock(self.inner.lock(), "EpisodicStore::recent_episodes");
2182        let mut items: Vec<MemoryItem> = inner
2183            .items
2184            .get(agent_id)
2185            .map(|v| v.iter().cloned().collect())
2186            .unwrap_or_default();
2187        items.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
2188        items.truncate(n);
2189        Ok(items)
2190    }
2191
2192    /// Return the most recently stored episode for `agent_id` (largest timestamp), or
2193    /// `None` if the agent has no stored episodes.
2194    pub fn most_recent_episode(
2195        &self,
2196        agent_id: &AgentId,
2197    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
2198        let inner = recover_lock(self.inner.lock(), "EpisodicStore::most_recent_episode");
2199        Ok(inner
2200            .items
2201            .get(agent_id)
2202            .and_then(|v| v.iter().max_by_key(|i| i.timestamp))
2203            .cloned())
2204    }
2205
2206    /// Return the episode with the highest `recall_count` for `agent_id`, or `None`
2207    /// if the agent has no stored episodes.  Ties are broken in favour of the
2208    /// episode with the higher importance score.
2209    pub fn most_recalled_episode(
2210        &self,
2211        agent_id: &AgentId,
2212    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
2213        let inner = recover_lock(self.inner.lock(), "EpisodicStore::most_recalled_episode");
2214        Ok(inner
2215            .items
2216            .get(agent_id)
2217            .and_then(|v| {
2218                v.iter().max_by(|a, b| {
2219                    a.recall_count.cmp(&b.recall_count).then_with(|| {
2220                        a.importance
2221                            .partial_cmp(&b.importance)
2222                            .unwrap_or(std::cmp::Ordering::Equal)
2223                    })
2224                })
2225            })
2226            .cloned())
2227    }
2228
2229    /// Return the episode with the highest importance score for `agent_id`, or `None`
2230    /// if the agent has no stored episodes.  Ties are broken in favour of the
2231    /// later-inserted episode.
2232    pub fn max_importance_episode(
2233        &self,
2234        agent_id: &AgentId,
2235    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
2236        let inner = recover_lock(self.inner.lock(), "EpisodicStore::max_importance_episode");
2237        let item = inner
2238            .items
2239            .get(agent_id)
2240            .and_then(|v| {
2241                v.iter()
2242                    .max_by(|a, b| {
2243                        a.importance
2244                            .partial_cmp(&b.importance)
2245                            .unwrap_or(std::cmp::Ordering::Equal)
2246                    })
2247            })
2248            .cloned();
2249        Ok(item)
2250    }
2251
2252    /// Return the episode with the lowest importance score for `agent_id`, or
2253    /// `None` if the agent has no stored episodes.  Ties are broken in favour
2254    /// of the later-inserted episode.
2255    pub fn min_importance_episode(
2256        &self,
2257        agent_id: &AgentId,
2258    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
2259        let inner = recover_lock(self.inner.lock(), "EpisodicStore::min_importance_episode");
2260        let item = inner
2261            .items
2262            .get(agent_id)
2263            .and_then(|v| {
2264                v.iter().min_by(|a, b| {
2265                    a.importance
2266                        .partial_cmp(&b.importance)
2267                        .unwrap_or(std::cmp::Ordering::Equal)
2268                })
2269            })
2270            .cloned();
2271        Ok(item)
2272    }
2273
2274    /// Return all episodes for `agent_id` sorted by importance descending
2275    /// (highest first).
2276    ///
2277    /// Returns an empty `Vec` for an unknown agent.
2278    pub fn episodes_sorted_by_importance(
2279        &self,
2280        agent_id: &AgentId,
2281    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
2282        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episodes_sorted_by_importance");
2283        let mut items: Vec<MemoryItem> = inner
2284            .items
2285            .get(agent_id)
2286            .map(|v| v.iter().cloned().collect())
2287            .unwrap_or_default();
2288        items.sort_by(|a, b| {
2289            b.importance
2290                .partial_cmp(&a.importance)
2291                .unwrap_or(std::cmp::Ordering::Equal)
2292        });
2293        Ok(items)
2294    }
2295
2296    /// Return the most recently inserted episode for `agent_id`, or `None`
2297    /// if the agent has no stored episodes.
2298    pub fn newest(
2299        &self,
2300        agent_id: &AgentId,
2301    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
2302        let inner = recover_lock(self.inner.lock(), "EpisodicStore::newest");
2303        let item = inner.items.get(agent_id).and_then(|v| v.last()).cloned();
2304        Ok(item)
2305    }
2306
2307    /// Return the total number of stored episodes across all agents.
2308    pub fn len(&self) -> Result<usize, AgentRuntimeError> {
2309        let inner = recover_lock(self.inner.lock(), "EpisodicStore::len");
2310        Ok(inner.items.values().map(|v| v.len()).sum())
2311    }
2312
2313    /// Return `true` if no episodes have been stored.
2314    pub fn is_empty(&self) -> Result<bool, AgentRuntimeError> {
2315        Ok(self.len()? == 0)
2316    }
2317
2318    /// Return the number of distinct agents that have at least one stored episode.
2319    pub fn agent_count(&self) -> Result<usize, AgentRuntimeError> {
2320        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agent_count");
2321        Ok(inner.items.len())
2322    }
2323
2324    /// Return the number of stored episodes for a specific agent.
2325    ///
2326    /// Returns `0` if the agent has no episodes or has not been seen before.
2327    pub fn agent_memory_count(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
2328        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agent_memory_count");
2329        Ok(inner.items.get(agent_id).map_or(0, |v| v.len()))
2330    }
2331
2332    /// Return `true` if `agent_id` has at least one stored episode.
2333    pub fn has_episodes(&self, agent_id: &AgentId) -> Result<bool, AgentRuntimeError> {
2334        let inner = recover_lock(self.inner.lock(), "EpisodicStore::has_episodes");
2335        Ok(inner
2336            .items
2337            .get(agent_id)
2338            .map_or(false, |v| !v.is_empty()))
2339    }
2340
2341    /// Return the most recently inserted episode for `agent_id`, or `None` if
2342    /// the agent has no stored episodes.
2343    ///
2344    /// "Most recent" is determined by `timestamp`.
2345    pub fn latest_episode(
2346        &self,
2347        agent_id: &AgentId,
2348    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
2349        let inner = recover_lock(self.inner.lock(), "EpisodicStore::latest_episode");
2350        Ok(inner
2351            .items
2352            .get(agent_id)
2353            .and_then(|v| v.iter().max_by_key(|i| i.timestamp))
2354            .cloned())
2355    }
2356
2357    /// Return the maximum single `recall_count` value across all episodes for
2358    /// `agent_id`, or `None` if the agent has no stored episodes.
2359    pub fn max_recall_count_for(&self, agent_id: &AgentId) -> Result<Option<u64>, AgentRuntimeError> {
2360        let inner = recover_lock(self.inner.lock(), "EpisodicStore::max_recall_count_for");
2361        Ok(inner
2362            .items
2363            .get(agent_id)
2364            .and_then(|v| v.iter().map(|i| i.recall_count).max()))
2365    }
2366
2367    /// Return the mean importance score across all episodes for `agent_id`.
2368    ///
2369    /// Returns `0.0` when the agent has no stored episodes.
2370    pub fn avg_importance(&self, agent_id: &AgentId) -> Result<f64, AgentRuntimeError> {
2371        let inner = recover_lock(self.inner.lock(), "EpisodicStore::avg_importance");
2372        let episodes = match inner.items.get(agent_id) {
2373            Some(v) if !v.is_empty() => v,
2374            _ => return Ok(0.0),
2375        };
2376        let sum: f64 = episodes.iter().map(|i| f64::from(i.importance)).sum();
2377        Ok(sum / episodes.len() as f64)
2378    }
2379
2380    /// Return the `(min, max)` importance pair across all episodes for
2381    /// `agent_id`, or `None` when the agent has no stored episodes.
2382    pub fn importance_range(
2383        &self,
2384        agent_id: &AgentId,
2385    ) -> Result<Option<(f32, f32)>, AgentRuntimeError> {
2386        let inner = recover_lock(self.inner.lock(), "EpisodicStore::importance_range");
2387        Ok(inner.items.get(agent_id).and_then(|v| {
2388            if v.is_empty() {
2389                return None;
2390            }
2391            let min = v
2392                .iter()
2393                .map(|i| i.importance)
2394                .fold(f32::INFINITY, f32::min);
2395            let max = v
2396                .iter()
2397                .map(|i| i.importance)
2398                .fold(f32::NEG_INFINITY, f32::max);
2399            Some((min, max))
2400        }))
2401    }
2402
2403    /// Return the total `recall_count` across all episodes for `agent_id`.
2404    ///
2405    /// Returns `0` if the agent has no stored episodes.
2406    pub fn sum_recall_counts(&self, agent_id: &AgentId) -> Result<u64, AgentRuntimeError> {
2407        let inner = recover_lock(self.inner.lock(), "EpisodicStore::sum_recall_counts");
2408        Ok(inner
2409            .items
2410            .get(agent_id)
2411            .map(|v| v.iter().map(|i| i.recall_count).sum())
2412            .unwrap_or(0))
2413    }
2414
2415    /// Return all agent IDs that have at least one stored episode.
2416    ///
2417    /// The order of agents in the returned vector is not guaranteed.
2418    pub fn list_agents(&self) -> Result<Vec<AgentId>, AgentRuntimeError> {
2419        let inner = recover_lock(self.inner.lock(), "EpisodicStore::list_agents");
2420        Ok(inner.items.keys().cloned().collect())
2421    }
2422
2423    /// Remove all stored episodes for `agent_id` and return the number removed.
2424    ///
2425    /// Returns `0` if the agent had no episodes.  Does not affect other agents.
2426    pub fn purge_agent_memories(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
2427        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::purge_agent_memories");
2428        let removed = inner.items.remove(agent_id).map_or(0, |v| v.len());
2429        Ok(removed)
2430    }
2431
2432    /// Remove all memories for the given agent.
2433    ///
2434    /// After this call, `recall` for this agent returns an empty list.
2435    pub fn clear_agent_memory(&self, agent_id: &AgentId) -> Result<(), AgentRuntimeError> {
2436        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::clear_agent_memory");
2437        inner.items.remove(agent_id);
2438        Ok(())
2439    }
2440
2441    /// Remove **all** episodes for **all** agents.
2442    ///
2443    /// After this call `len()` returns `0` and `list_agents()` returns an empty slice.
2444    pub fn clear_all(&self) -> Result<(), AgentRuntimeError> {
2445        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::clear_all");
2446        inner.items.clear();
2447        Ok(())
2448    }
2449
2450    /// Export all memories for the given agent as a serializable Vec.
2451    ///
2452    /// Useful for migrating agent state across runtime instances.
2453    pub fn export_agent_memory(&self, agent_id: &AgentId) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
2454        let inner = recover_lock(self.inner.lock(), "EpisodicStore::export_agent_memory");
2455        Ok(inner.items.get(agent_id).cloned().unwrap_or_default())
2456    }
2457
2458    /// Import a Vec of MemoryItems for the given agent, replacing any existing memories.
2459    ///
2460    /// The agent's existing memories are completely replaced by the imported items.
2461    pub fn import_agent_memory(&self, agent_id: &AgentId, items: Vec<MemoryItem>) -> Result<(), AgentRuntimeError> {
2462        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::import_agent_memory");
2463        inner.items.insert(agent_id.clone(), items);
2464        Ok(())
2465    }
2466
2467    /// Bump the `recall_count` of every item whose content equals `content` by `amount`.
2468    ///
2469    /// This method exists to support integration tests that need to simulate prior recall
2470    /// history without accessing private fields. It is not intended for production use.
2471    #[doc(hidden)]
2472    pub fn bump_recall_count_by_content(&self, content: &str, amount: u64) {
2473        let mut inner = recover_lock(
2474            self.inner.lock(),
2475            "EpisodicStore::bump_recall_count_by_content",
2476        );
2477        for agent_items in inner.items.values_mut() {
2478            for item in agent_items.iter_mut() {
2479                if item.content == content {
2480                    item.recall_count = item.recall_count.saturating_add(amount);
2481                }
2482            }
2483        }
2484    }
2485
2486    /// Search episodes for a given `agent_id` whose content contains `query` as a substring.
2487    ///
2488    /// The comparison is case-sensitive.  Returns at most `limit` matching items,
2489    /// ordered by descending importance (same as [`recall`]).
2490    ///
2491    /// [`recall`]: EpisodicStore::recall
2492    pub fn search_by_content(
2493        &self,
2494        agent_id: &AgentId,
2495        query: &str,
2496        limit: usize,
2497    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
2498        let inner = recover_lock(self.inner.lock(), "EpisodicStore::search_by_content");
2499        let items = inner.items.get(agent_id).cloned().unwrap_or_default();
2500        drop(inner);
2501        let mut matched: Vec<MemoryItem> = items
2502            .into_iter()
2503            .filter(|item| item.content.contains(query))
2504            .collect();
2505        matched.sort_unstable_by(|a, b| b.importance.partial_cmp(&a.importance).unwrap_or(std::cmp::Ordering::Equal));
2506        if limit > 0 {
2507            matched.truncate(limit);
2508        }
2509        Ok(matched)
2510    }
2511
2512    /// Return the count of episodes for `agent_id` whose importance is strictly
2513    /// above `threshold`.
2514    ///
2515    /// Returns `0` for unknown agents or when no episodes exceed the threshold.
2516    pub fn episode_count_above_importance(
2517        &self,
2518        agent_id: &AgentId,
2519        threshold: f32,
2520    ) -> Result<usize, AgentRuntimeError> {
2521        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episode_count_above_importance");
2522        let count = inner
2523            .items
2524            .get(agent_id)
2525            .map_or(0, |items| items.iter().filter(|m| m.importance > threshold).count());
2526        Ok(count)
2527    }
2528
2529    /// Return the mean importance score of all episodes for `agent_id`.
2530    ///
2531    /// Returns `0.0` for unknown agents or agents with no episodes.
2532    pub fn avg_episode_importance(
2533        &self,
2534        agent_id: &AgentId,
2535    ) -> Result<f64, AgentRuntimeError> {
2536        let inner = recover_lock(self.inner.lock(), "EpisodicStore::avg_episode_importance");
2537        let items = match inner.items.get(agent_id) {
2538            Some(items) if !items.is_empty() => items,
2539            _ => return Ok(0.0),
2540        };
2541        let sum: f64 = items.iter().map(|m| m.importance as f64).sum();
2542        Ok(sum / items.len() as f64)
2543    }
2544
2545    /// Return the total byte count of all episode content strings for
2546    /// `agent_id`.
2547    ///
2548    /// Returns `0` for unknown agents.
2549    pub fn episode_content_bytes_total(
2550        &self,
2551        agent_id: &AgentId,
2552    ) -> Result<usize, AgentRuntimeError> {
2553        let inner = recover_lock(
2554            self.inner.lock(),
2555            "EpisodicStore::episode_content_bytes_total",
2556        );
2557        Ok(inner
2558            .items
2559            .get(agent_id)
2560            .map_or(0, |items| items.iter().map(|m| m.content.len()).sum()))
2561    }
2562
2563    /// Return the total number of whitespace-separated words across all episode
2564    /// content strings for `agent_id`.
2565    ///
2566    /// Returns `0` for unknown agents or agents with no episodes.
2567    pub fn total_content_words(
2568        &self,
2569        agent_id: &AgentId,
2570    ) -> Result<usize, AgentRuntimeError> {
2571        let inner = recover_lock(self.inner.lock(), "EpisodicStore::total_content_words");
2572        Ok(inner
2573            .items
2574            .get(agent_id)
2575            .map_or(0, |items| {
2576                items
2577                    .iter()
2578                    .map(|m| m.content.split_whitespace().count())
2579                    .sum()
2580            }))
2581    }
2582
2583    /// Return the minimum importance score across all episodes for `agent_id`.
2584    ///
2585    /// Returns `None` when the agent has no episodes.  Complements
2586    /// [`avg_episode_importance`](Self::avg_episode_importance).
2587    pub fn min_episode_importance(
2588        &self,
2589        agent_id: &AgentId,
2590    ) -> Result<Option<f32>, AgentRuntimeError> {
2591        let inner =
2592            recover_lock(self.inner.lock(), "EpisodicStore::min_episode_importance");
2593        Ok(inner.items.get(agent_id).and_then(|items| {
2594            items
2595                .iter()
2596                .map(|m| m.importance)
2597                .reduce(f32::min)
2598        }))
2599    }
2600
2601    /// Return the count of episodes for `agent_id` whose importance is
2602    /// strictly greater than `threshold`.
2603    ///
2604    /// Returns `0` for unknown agents.
2605    pub fn episodes_above_importance_count(
2606        &self,
2607        agent_id: &AgentId,
2608        threshold: f32,
2609    ) -> Result<usize, AgentRuntimeError> {
2610        let inner =
2611            recover_lock(self.inner.lock(), "EpisodicStore::episodes_above_importance_count");
2612        Ok(inner
2613            .items
2614            .get(agent_id)
2615            .map_or(0, |items| items.iter().filter(|m| m.importance > threshold).count()))
2616    }
2617
2618    /// Return the union of all tags across all episodes for `agent_id`.
2619    ///
2620    /// Returns an empty set for unknown agents or when all episodes have no
2621    /// tags.
2622    pub fn tag_union(
2623        &self,
2624        agent_id: &AgentId,
2625    ) -> Result<std::collections::HashSet<String>, AgentRuntimeError> {
2626        let inner = recover_lock(self.inner.lock(), "EpisodicStore::tag_union");
2627        Ok(inner
2628            .items
2629            .get(agent_id)
2630            .map_or_else(std::collections::HashSet::new, |items| {
2631                items.iter().flat_map(|m| m.tags.iter().cloned()).collect()
2632            }))
2633    }
2634
2635    /// Return the most recently stored episode for `agent_id`.
2636    ///
2637    /// Uses `MemoryItem::timestamp` to determine recency.  Returns `None` for
2638    /// unknown agents.
2639    pub fn episode_most_recent(
2640        &self,
2641        agent_id: &AgentId,
2642    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
2643        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episode_most_recent");
2644        Ok(inner
2645            .items
2646            .get(agent_id)
2647            .and_then(|items| items.iter().max_by_key(|m| m.timestamp))
2648            .cloned())
2649    }
2650
2651    /// Return all episodes for `agent_id` whose importance is in `[lo, hi]`
2652    /// (inclusive), sorted by importance ascending.
2653    ///
2654    /// Returns an empty `Vec` for unknown agents.
2655    pub fn episodes_by_importance_range(
2656        &self,
2657        agent_id: &AgentId,
2658        lo: f32,
2659        hi: f32,
2660    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
2661        let inner =
2662            recover_lock(self.inner.lock(), "EpisodicStore::episodes_by_importance_range");
2663        let mut items: Vec<MemoryItem> = inner
2664            .items
2665            .get(agent_id)
2666            .map_or_else(Vec::new, |items| {
2667                items
2668                    .iter()
2669                    .filter(|m| m.importance >= lo && m.importance <= hi)
2670                    .cloned()
2671                    .collect()
2672            });
2673        items.sort_unstable_by(|a, b| {
2674            a.importance.partial_cmp(&b.importance).unwrap_or(std::cmp::Ordering::Equal)
2675        });
2676        Ok(items)
2677    }
2678
2679    /// Return the number of episodes for `agent_id` whose timestamp falls
2680    /// within `[start, end]` (inclusive on both ends).
2681    ///
2682    /// Returns `0` for unknown agents or when no episode falls in the window.
2683    pub fn count_episodes_in_window(
2684        &self,
2685        agent_id: &AgentId,
2686        start: chrono::DateTime<chrono::Utc>,
2687        end: chrono::DateTime<chrono::Utc>,
2688    ) -> Result<usize, AgentRuntimeError> {
2689        let inner =
2690            recover_lock(self.inner.lock(), "EpisodicStore::count_episodes_in_window");
2691        Ok(inner.items.get(agent_id).map_or(0, |items| {
2692            items
2693                .iter()
2694                .filter(|m| m.timestamp >= start && m.timestamp <= end)
2695                .count()
2696        }))
2697    }
2698
2699    /// Return the total number of tags across all episodes for `agent_id`.
2700    ///
2701    /// Episodes with no tags contribute `0` to the sum.  Returns `0` for
2702    /// unknown agents.
2703    pub fn total_tag_count(
2704        &self,
2705        agent_id: &AgentId,
2706    ) -> Result<usize, AgentRuntimeError> {
2707        let inner = recover_lock(self.inner.lock(), "EpisodicStore::total_tag_count");
2708        Ok(inner
2709            .items
2710            .get(agent_id)
2711            .map_or(0, |items| items.iter().map(|m| m.tags.len()).sum()))
2712    }
2713
2714    /// Return the average number of tags per episode for `agent_id`.
2715    ///
2716    /// Returns `0.0` for unknown agents or when there are no episodes.
2717    pub fn avg_tag_count_per_episode(
2718        &self,
2719        agent_id: &AgentId,
2720    ) -> Result<f64, AgentRuntimeError> {
2721        let inner =
2722            recover_lock(self.inner.lock(), "EpisodicStore::avg_tag_count_per_episode");
2723        Ok(inner.items.get(agent_id).map_or(0.0, |items| {
2724            if items.is_empty() {
2725                return 0.0;
2726            }
2727            let total: usize = items.iter().map(|m| m.tags.len()).sum();
2728            total as f64 / items.len() as f64
2729        }))
2730    }
2731
2732    /// Return all episodes for `agent_id` whose importance is strictly below
2733    /// `threshold`, sorted by importance ascending (least important first).
2734    ///
2735    /// Returns an empty `Vec` for unknown agents or when all episodes meet the
2736    /// threshold.
2737    pub fn low_importance_episodes(
2738        &self,
2739        agent_id: &AgentId,
2740        threshold: f32,
2741    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
2742        let inner = recover_lock(self.inner.lock(), "EpisodicStore::low_importance_episodes");
2743        let mut items: Vec<MemoryItem> = inner
2744            .items
2745            .get(agent_id)
2746            .map_or_else(Vec::new, |items| {
2747                items.iter().filter(|m| m.importance < threshold).cloned().collect()
2748            });
2749        items.sort_unstable_by(|a, b| {
2750            a.importance.partial_cmp(&b.importance).unwrap_or(std::cmp::Ordering::Equal)
2751        });
2752        Ok(items)
2753    }
2754
2755    /// Return all episodes for `agent_id` sorted by timestamp ascending
2756    /// (oldest first).
2757    ///
2758    /// Returns an empty `Vec` for unknown agents.
2759    pub fn episodes_sorted_by_timestamp(
2760        &self,
2761        agent_id: &AgentId,
2762    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
2763        let inner = recover_lock(
2764            self.inner.lock(),
2765            "EpisodicStore::episodes_sorted_by_timestamp",
2766        );
2767        let mut items: Vec<MemoryItem> = inner
2768            .items
2769            .get(agent_id)
2770            .map_or_else(Vec::new, |v| v.clone());
2771        items.sort_unstable_by_key(|m| m.timestamp);
2772        Ok(items)
2773    }
2774
2775    /// Return a sorted, deduplicated list of all tags used by any episode
2776    /// belonging to `agent_id`.
2777    ///
2778    /// Returns an empty `Vec` for unknown agents.
2779    pub fn all_unique_tags(
2780        &self,
2781        agent_id: &AgentId,
2782    ) -> Result<Vec<String>, AgentRuntimeError> {
2783        let inner = recover_lock(self.inner.lock(), "EpisodicStore::all_unique_tags");
2784        let mut tags: Vec<String> = inner
2785            .items
2786            .get(agent_id)
2787            .map_or_else(Vec::new, |items| {
2788                items.iter().flat_map(|m| m.tags.iter().cloned()).collect()
2789            });
2790        tags.sort_unstable();
2791        tags.dedup();
2792        Ok(tags)
2793    }
2794
2795    /// Return all episodes for `agent_id` that contain `tag` in their tag list.
2796    ///
2797    /// Returns an empty `Vec` for unknown agents or when no episode has the tag.
2798    pub fn episodes_with_tag(
2799        &self,
2800        agent_id: &AgentId,
2801        tag: &str,
2802    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
2803        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episodes_with_tag");
2804        Ok(inner
2805            .items
2806            .get(agent_id)
2807            .map_or_else(Vec::new, |items| {
2808                items
2809                    .iter()
2810                    .filter(|m| m.tags.iter().any(|t| t == tag))
2811                    .cloned()
2812                    .collect()
2813            }))
2814    }
2815
2816    /// Return the count of episodes for `agent_id` whose timestamp is strictly
2817    /// before `before`.
2818    ///
2819    /// Returns `0` for unknown agents or when no episode qualifies.
2820    pub fn episode_count_before(
2821        &self,
2822        agent_id: &AgentId,
2823        before: chrono::DateTime<chrono::Utc>,
2824    ) -> Result<usize, AgentRuntimeError> {
2825        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episode_count_before");
2826        Ok(inner
2827            .items
2828            .get(agent_id)
2829            .map_or(0, |items| {
2830                items.iter().filter(|m| m.timestamp < before).count()
2831            }))
2832    }
2833
2834    /// Return the IDs of all episodes stored for `agent_id`.
2835    ///
2836    /// The order of the returned IDs is unspecified.  Returns an empty `Vec`
2837    /// for unknown agents.
2838    pub fn episode_ids(&self, agent_id: &AgentId) -> Result<Vec<MemoryId>, AgentRuntimeError> {
2839        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episode_ids");
2840        Ok(inner
2841            .items
2842            .get(agent_id)
2843            .map_or_else(Vec::new, |items| items.iter().map(|m| m.id.clone()).collect()))
2844    }
2845
2846    /// Return all episodes for `agent_id` whose importance score is strictly
2847    /// greater than `threshold`.
2848    ///
2849    /// Returns an empty `Vec` for unknown agents or when no episode qualifies.
2850    pub fn episodes_above_importance(
2851        &self,
2852        agent_id: &AgentId,
2853        threshold: f32,
2854    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
2855        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episodes_above_importance");
2856        Ok(inner
2857            .items
2858            .get(agent_id)
2859            .map_or_else(Vec::new, |items| {
2860                items.iter().filter(|m| m.importance > threshold).cloned().collect()
2861            }))
2862    }
2863
2864    /// Return all episodes for `agent_id` whose timestamp falls in the range
2865    /// `[from, to)` (inclusive start, exclusive end).
2866    ///
2867    /// Returns an empty `Vec` for unknown agents or when no episode qualifies.
2868    pub fn episodes_between(
2869        &self,
2870        agent_id: &AgentId,
2871        from: chrono::DateTime<chrono::Utc>,
2872        to: chrono::DateTime<chrono::Utc>,
2873    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
2874        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episodes_between");
2875        Ok(inner
2876            .items
2877            .get(agent_id)
2878            .map_or_else(Vec::new, |items| {
2879                items
2880                    .iter()
2881                    .filter(|m| m.timestamp >= from && m.timestamp < to)
2882                    .cloned()
2883                    .collect()
2884            }))
2885    }
2886
2887    /// Return all episodes for `agent_id` that carry **every** tag in `tags`.
2888    ///
2889    /// Returns an empty `Vec` when `tags` is empty, the agent is unknown, or
2890    /// no episode satisfies all of the given tags.
2891    pub fn episodes_tagged_with_all(
2892        &self,
2893        agent_id: &AgentId,
2894        tags: &[&str],
2895    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
2896        if tags.is_empty() {
2897            return Ok(Vec::new());
2898        }
2899        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episodes_tagged_with_all");
2900        Ok(inner
2901            .items
2902            .get(agent_id)
2903            .map_or_else(Vec::new, |items| {
2904                items
2905                    .iter()
2906                    .filter(|m| tags.iter().all(|t| m.has_tag(t)))
2907                    .cloned()
2908                    .collect()
2909            }))
2910    }
2911
2912    /// Return the total whitespace-delimited word count across all episodes
2913    /// for `agent_id`.
2914    ///
2915    /// Returns `0` for unknown agents or agents with no episodes.
2916    pub fn content_word_count_total(
2917        &self,
2918        agent_id: &AgentId,
2919    ) -> Result<usize, AgentRuntimeError> {
2920        let inner =
2921            recover_lock(self.inner.lock(), "EpisodicStore::content_word_count_total");
2922        Ok(inner
2923            .items
2924            .get(agent_id)
2925            .map_or(0, |items| {
2926                items.iter().map(|m| m.word_count()).sum()
2927            }))
2928    }
2929
2930    /// Return the sum of importance scores for all episodes belonging to
2931    /// `agent_id`.
2932    ///
2933    /// Returns `0.0` for unknown agents or agents with no episodes.
2934    pub fn weighted_importance_sum(
2935        &self,
2936        agent_id: &AgentId,
2937    ) -> Result<f32, AgentRuntimeError> {
2938        let inner = recover_lock(self.inner.lock(), "EpisodicStore::weighted_importance_sum");
2939        Ok(inner
2940            .items
2941            .get(agent_id)
2942            .map_or(0.0, |items| items.iter().map(|m| m.importance).sum()))
2943    }
2944
2945    /// Return a `Vec` of content byte lengths for all episodes of `agent_id`.
2946    ///
2947    /// The order matches the internal storage order (insertion order).
2948    /// Returns an empty `Vec` for unknown agents.
2949    pub fn episode_content_lengths(
2950        &self,
2951        agent_id: &AgentId,
2952    ) -> Result<Vec<usize>, AgentRuntimeError> {
2953        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episode_content_lengths");
2954        Ok(inner
2955            .items
2956            .get(agent_id)
2957            .map_or_else(Vec::new, |items| items.iter().map(|m| m.content.len()).collect()))
2958    }
2959
2960    /// Return the number of distinct agents that have at least one episode.
2961    ///
2962    /// Provides a quick count of how many agents have recorded any episodic
2963    /// memory.  Returns `0` for an empty store.
2964    pub fn episode_count_by_agent(&self) -> Result<std::collections::HashMap<AgentId, usize>, AgentRuntimeError> {
2965        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episode_count_by_agent");
2966        Ok(inner.items.iter().map(|(id, items)| (id.clone(), items.len())).collect())
2967    }
2968
2969    /// Return all episode IDs across every agent in the store.
2970    ///
2971    /// The returned `Vec` is unsorted.  Returns an empty `Vec` for an empty
2972    /// store.
2973    pub fn all_episode_ids(&self) -> Result<Vec<MemoryId>, AgentRuntimeError> {
2974        let inner = recover_lock(self.inner.lock(), "EpisodicStore::all_episode_ids");
2975        let ids: Vec<MemoryId> = inner
2976            .items
2977            .values()
2978            .flat_map(|items| items.iter().map(|m| m.id.clone()))
2979            .collect();
2980        Ok(ids)
2981    }
2982
2983    /// Return all episodes for `agent_id` whose content byte length exceeds `min_bytes`.
2984    ///
2985    /// Returns an empty `Vec` when the agent has no episodes or none exceed the
2986    /// threshold.
2987    pub fn episodes_above_content_bytes(
2988        &self,
2989        agent_id: &AgentId,
2990        min_bytes: usize,
2991    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
2992        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episodes_above_content_bytes");
2993        Ok(inner
2994            .items
2995            .get(agent_id)
2996            .map_or_else(Vec::new, |items| {
2997                items.iter().filter(|m| m.content.len() > min_bytes).cloned().collect()
2998            }))
2999    }
3000
3001    /// Return all agent IDs sorted by episode count in descending order.
3002    ///
3003    /// Agents with more episodes appear first.  When two agents have the same
3004    /// count the tie is broken alphabetically by agent ID.
3005    pub fn agents_sorted_by_episode_count(&self) -> Result<Vec<AgentId>, AgentRuntimeError> {
3006        let inner =
3007            recover_lock(self.inner.lock(), "EpisodicStore::agents_sorted_by_episode_count");
3008        let mut pairs: Vec<(AgentId, usize)> = inner
3009            .items
3010            .iter()
3011            .map(|(id, items)| (id.clone(), items.len()))
3012            .collect();
3013        pairs.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.as_str().cmp(b.0.as_str())));
3014        Ok(pairs.into_iter().map(|(id, _)| id).collect())
3015    }
3016
3017    /// Return the number of distinct agents that have at least one episode.
3018    ///
3019    /// Returns `0` for an empty store.
3020    pub fn unique_agents_count(&self) -> Result<usize, AgentRuntimeError> {
3021        let inner = recover_lock(self.inner.lock(), "EpisodicStore::unique_agents_count");
3022        Ok(inner.items.len())
3023    }
3024
3025    /// Return the number of episodes for `agent_id` that have at least one tag.
3026    ///
3027    /// Returns `0` for an unknown agent or an empty store.
3028    pub fn episodes_with_tag_count(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
3029        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episodes_with_tag_count");
3030        Ok(inner
3031            .items
3032            .get(agent_id)
3033            .map_or(0, |items| items.iter().filter(|m| !m.tags.is_empty()).count()))
3034    }
3035
3036    /// Return all episodes for `agent_id` whose content has at least `min_words`
3037    /// whitespace-delimited words.
3038    ///
3039    /// Returns an empty `Vec` for unknown agents or when no episode qualifies.
3040    pub fn episodes_with_min_word_count(
3041        &self,
3042        agent_id: &AgentId,
3043        min_words: usize,
3044    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
3045        let inner =
3046            recover_lock(self.inner.lock(), "EpisodicStore::episodes_with_min_word_count");
3047        Ok(inner
3048            .items
3049            .get(agent_id)
3050            .map_or_else(Vec::new, |items| {
3051                items
3052                    .iter()
3053                    .filter(|m| m.word_count() >= min_words)
3054                    .cloned()
3055                    .collect()
3056            }))
3057    }
3058
3059    /// Return the episode with the most tags for `agent_id`, or `None` when
3060    /// the agent has no episodes.  Ties are broken by insertion order.
3061    pub fn most_tagged_episode(
3062        &self,
3063        agent_id: &AgentId,
3064    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
3065        let inner =
3066            recover_lock(self.inner.lock(), "EpisodicStore::most_tagged_episode");
3067        Ok(inner
3068            .items
3069            .get(agent_id)
3070            .and_then(|items| items.iter().max_by_key(|m| m.tag_count()).cloned()))
3071    }
3072
3073    /// Return a map of tag → episode count for all episodes of `agent_id`.
3074    ///
3075    /// Each entry reports how many episodes carry that particular tag.
3076    /// Returns an empty map for unknown agents or agents with no tagged episodes.
3077    pub fn tag_frequency(
3078        &self,
3079        agent_id: &AgentId,
3080    ) -> Result<std::collections::HashMap<String, usize>, AgentRuntimeError> {
3081        let inner = recover_lock(self.inner.lock(), "EpisodicStore::tag_frequency");
3082        let mut freq: std::collections::HashMap<String, usize> =
3083            std::collections::HashMap::new();
3084        if let Some(items) = inner.items.get(agent_id) {
3085            for item in items {
3086                for tag in &item.tags {
3087                    *freq.entry(tag.clone()).or_insert(0) += 1;
3088                }
3089            }
3090        }
3091        Ok(freq)
3092    }
3093
3094    /// Return the episode with the highest `importance` score for `agent_id`.
3095    ///
3096    /// When multiple episodes share the maximum importance score the first one
3097    /// encountered is returned.  Returns `None` for unknown agents or an empty
3098    /// store.
3099    pub fn most_important_episode(
3100        &self,
3101        agent_id: &AgentId,
3102    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
3103        let inner = recover_lock(self.inner.lock(), "EpisodicStore::most_important_episode");
3104        Ok(inner
3105            .items
3106            .get(agent_id)
3107            .and_then(|items| {
3108                items
3109                    .iter()
3110                    .max_by(|a, b| {
3111                        a.importance
3112                            .partial_cmp(&b.importance)
3113                            .unwrap_or(std::cmp::Ordering::Equal)
3114                    })
3115                    .cloned()
3116            }))
3117    }
3118
3119    /// Return the number of episodes for `agent_id` that carry `tag`.
3120    ///
3121    /// Returns `0` for unknown agents or if no episode carries `tag`.
3122    pub fn episode_count_with_tag(
3123        &self,
3124        agent_id: &AgentId,
3125        tag: &str,
3126    ) -> Result<usize, AgentRuntimeError> {
3127        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episode_count_with_tag");
3128        Ok(inner
3129            .items
3130            .get(agent_id)
3131            .map_or(0, |items| items.iter().filter(|m| m.has_tag(tag)).count()))
3132    }
3133
3134    /// Return all episodes for `agent_id` whose `importance` is strictly below
3135    /// `threshold`.
3136    ///
3137    /// Returns an empty `Vec` for unknown agents or when no episode qualifies.
3138    pub fn episodes_below_importance(
3139        &self,
3140        agent_id: &AgentId,
3141        threshold: f32,
3142    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
3143        let inner =
3144            recover_lock(self.inner.lock(), "EpisodicStore::episodes_below_importance");
3145        Ok(inner
3146            .items
3147            .get(agent_id)
3148            .map_or_else(Vec::new, |items| {
3149                items.iter().filter(|m| m.importance < threshold).cloned().collect()
3150            }))
3151    }
3152
3153    /// Return `true` if at least one episode has been stored for `agent_id`.
3154    ///
3155    /// This is a cheaper alternative to calling [`EpisodicStore::recall`] and
3156    /// checking whether the result is empty.
3157    pub fn has_episodes_for_agent(
3158        &self,
3159        agent_id: &AgentId,
3160    ) -> Result<bool, AgentRuntimeError> {
3161        let inner =
3162            recover_lock(self.inner.lock(), "EpisodicStore::has_episodes_for_agent");
3163        Ok(inner
3164            .items
3165            .get(agent_id)
3166            .map_or(false, |items| !items.is_empty()))
3167    }
3168
3169    /// Return all episodes for `agent_id` whose content contains `substr`
3170    /// (case-sensitive substring match).
3171    ///
3172    /// Returns an empty `Vec` for unknown agents or when no episode matches.
3173    pub fn episodes_with_content_containing(
3174        &self,
3175        agent_id: &AgentId,
3176        substr: &str,
3177    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
3178        let inner = recover_lock(
3179            self.inner.lock(),
3180            "EpisodicStore::episodes_with_content_containing",
3181        );
3182        Ok(inner
3183            .items
3184            .get(agent_id)
3185            .map_or_else(Vec::new, |items| {
3186                items
3187                    .iter()
3188                    .filter(|m| m.content.contains(substr))
3189                    .cloned()
3190                    .collect()
3191            }))
3192    }
3193
3194    /// Return the maximum importance score across all episodes for `agent_id`,
3195    /// or `None` when the agent has no recorded episodes.
3196    pub fn max_episode_importance(
3197        &self,
3198        agent_id: &AgentId,
3199    ) -> Result<Option<f32>, AgentRuntimeError> {
3200        let inner =
3201            recover_lock(self.inner.lock(), "EpisodicStore::max_episode_importance");
3202        Ok(inner.items.get(agent_id).and_then(|v| {
3203            v.iter()
3204                .map(|m| m.importance)
3205                .reduce(f32::max)
3206        }))
3207    }
3208
3209}
3210
3211impl Default for EpisodicStore {
3212    fn default() -> Self {
3213        Self::new()
3214    }
3215}
3216
3217// ── SemanticStore ─────────────────────────────────────────────────────────────
3218
3219/// Stores semantic (fact-based) knowledge as tagged key-value pairs.
3220///
3221/// ## Guarantees
3222/// - Thread-safe via `Arc<Mutex<_>>`
3223/// - Retrieval by tag intersection
3224/// - Optional vector-based similarity search via stored embeddings
3225#[derive(Debug, Clone)]
3226pub struct SemanticStore {
3227    inner: Arc<Mutex<SemanticInner>>,
3228}
3229
3230#[derive(Debug)]
3231struct SemanticInner {
3232    entries: Vec<SemanticEntry>,
3233    expected_dim: Option<usize>,
3234}
3235
3236#[derive(Debug, Clone)]
3237struct SemanticEntry {
3238    key: String,
3239    value: String,
3240    tags: Vec<String>,
3241    embedding: Option<Vec<f32>>,
3242}
3243
3244impl SemanticStore {
3245    /// Create a new empty semantic store.
3246    pub fn new() -> Self {
3247        Self {
3248            inner: Arc::new(Mutex::new(SemanticInner {
3249                entries: Vec::new(),
3250                expected_dim: None,
3251            })),
3252        }
3253    }
3254
3255    /// Store a key-value pair with associated tags.
3256    #[tracing::instrument(skip(self))]
3257    pub fn store(
3258        &self,
3259        key: impl Into<String> + std::fmt::Debug,
3260        value: impl Into<String> + std::fmt::Debug,
3261        tags: Vec<String>,
3262    ) -> Result<(), AgentRuntimeError> {
3263        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::store");
3264        inner.entries.push(SemanticEntry {
3265            key: key.into(),
3266            value: value.into(),
3267            tags,
3268            embedding: None,
3269        });
3270        Ok(())
3271    }
3272
3273    /// Store a key-value pair with an embedding vector for similarity search.
3274    ///
3275    /// # Errors
3276    /// Returns `Err(AgentRuntimeError::Memory)` if `embedding` is empty or dimension mismatches.
3277    #[tracing::instrument(skip(self))]
3278    pub fn store_with_embedding(
3279        &self,
3280        key: impl Into<String> + std::fmt::Debug,
3281        value: impl Into<String> + std::fmt::Debug,
3282        tags: Vec<String>,
3283        embedding: Vec<f32>,
3284    ) -> Result<(), AgentRuntimeError> {
3285        if embedding.is_empty() {
3286            return Err(AgentRuntimeError::Memory(
3287                "embedding vector must not be empty".into(),
3288            ));
3289        }
3290        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::store_with_embedding");
3291        // Validate dimension consistency using expected_dim.
3292        if let Some(expected) = inner.expected_dim {
3293            if expected != embedding.len() {
3294                return Err(AgentRuntimeError::Memory(format!(
3295                    "embedding dimension mismatch: expected {expected}, got {}",
3296                    embedding.len()
3297                )));
3298            }
3299        } else {
3300            inner.expected_dim = Some(embedding.len());
3301        }
3302        // Pre-normalize so retrieve_similar can use dot product instead of
3303        // full cosine similarity for a ~3× speedup on large stores.
3304        let mut embedding = embedding;
3305        normalize_in_place(&mut embedding);
3306        inner.entries.push(SemanticEntry {
3307            key: key.into(),
3308            value: value.into(),
3309            tags,
3310            embedding: Some(embedding),
3311        });
3312        Ok(())
3313    }
3314
3315    /// Retrieve all entries that contain **all** of the given tags.
3316    ///
3317    /// If `tags` is empty, returns all entries.
3318    #[tracing::instrument(skip(self))]
3319    pub fn retrieve(&self, tags: &[&str]) -> Result<Vec<(String, String)>, AgentRuntimeError> {
3320        let inner = recover_lock(self.inner.lock(), "SemanticStore::retrieve");
3321
3322        let results = inner
3323            .entries
3324            .iter()
3325            .filter(|entry| {
3326                tags.iter()
3327                    .all(|t| entry.tags.iter().any(|et| et.as_str() == *t))
3328            })
3329            .map(|e| (e.key.clone(), e.value.clone()))
3330            .collect();
3331
3332        Ok(results)
3333    }
3334
3335    /// Retrieve top-k entries by cosine similarity to `query_embedding`.
3336    ///
3337    /// Only entries that were stored with an embedding (via [`store_with_embedding`])
3338    /// are considered.  Returns `(key, value, similarity)` sorted by descending
3339    /// similarity.
3340    ///
3341    /// Returns `Err(AgentRuntimeError::Memory)` if `query_embedding` dimension mismatches.
3342    ///
3343    /// [`store_with_embedding`]: SemanticStore::store_with_embedding
3344    #[tracing::instrument(skip(self, query_embedding))]
3345    pub fn retrieve_similar(
3346        &self,
3347        query_embedding: &[f32],
3348        top_k: usize,
3349    ) -> Result<Vec<(String, String, f32)>, AgentRuntimeError> {
3350        let inner = recover_lock(self.inner.lock(), "SemanticStore::retrieve_similar");
3351
3352        // Check dimension against expected_dim.
3353        if let Some(expected) = inner.expected_dim {
3354            if expected != query_embedding.len() {
3355                return Err(AgentRuntimeError::Memory(format!(
3356                    "query embedding dimension mismatch: expected {expected}, got {}",
3357                    query_embedding.len()
3358                )));
3359            }
3360        }
3361
3362        // Normalize the query so we can use dot product against pre-normalized
3363        // stored embeddings (equivalent to cosine similarity, but faster).
3364        let query_norm: f32 = query_embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
3365        if query_norm < f32::EPSILON {
3366            return Ok(vec![]);
3367        }
3368        let query_unit: Vec<f32> = query_embedding.iter().map(|x| x / query_norm).collect();
3369
3370        let mut scored: Vec<(String, String, f32)> = inner
3371            .entries
3372            .iter()
3373            .filter_map(|entry| {
3374                entry.embedding.as_ref().map(|emb| {
3375                    let sim = emb
3376                        .iter()
3377                        .zip(query_unit.iter())
3378                        .map(|(a, b)| a * b)
3379                        .sum::<f32>()
3380                        .clamp(-1.0, 1.0);
3381                    (entry.key.clone(), entry.value.clone(), sim)
3382                })
3383            })
3384            .collect();
3385
3386        // Partial sort: when top_k < total candidates, select_nth_unstable_by
3387        // partitions in O(n), then sort only the top top_k in O(top_k log top_k).
3388        let cmp = |a: &(String, String, f32), b: &(String, String, f32)| {
3389            b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)
3390        };
3391        if top_k > 0 && top_k < scored.len() {
3392            scored.select_nth_unstable_by(top_k - 1, cmp);
3393            scored[..top_k].sort_unstable_by(cmp);
3394        } else {
3395            scored.sort_unstable_by(cmp);
3396        }
3397        scored.truncate(top_k);
3398        Ok(scored)
3399    }
3400
3401    /// Update the `value` of the first entry whose key matches `key`.
3402    ///
3403    /// Returns `Ok(true)` if found and updated, `Ok(false)` if not found.
3404    pub fn update(&self, key: &str, new_value: impl Into<String>) -> Result<bool, AgentRuntimeError> {
3405        let new_value = new_value.into();
3406        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::update");
3407        if let Some(entry) = inner.entries.iter_mut().find(|e| e.key == key) {
3408            entry.value = new_value;
3409            Ok(true)
3410        } else {
3411            Ok(false)
3412        }
3413    }
3414
3415    /// Look up a single entry by its exact key.
3416    ///
3417    /// Returns `Ok(Some((value, tags)))` if found, `Ok(None)` if not.
3418    pub fn retrieve_by_key(&self, key: &str) -> Result<Option<(String, Vec<String>)>, AgentRuntimeError> {
3419        let inner = recover_lock(self.inner.lock(), "SemanticStore::retrieve_by_key");
3420        Ok(inner.entries.iter().find(|e| e.key == key).map(|e| (e.value.clone(), e.tags.clone())))
3421    }
3422
3423    /// Return `true` if an entry with the given key is stored.
3424    pub fn contains(&self, key: &str) -> Result<bool, AgentRuntimeError> {
3425        let inner = recover_lock(self.inner.lock(), "SemanticStore::contains");
3426        Ok(inner.entries.iter().any(|e| e.key == key))
3427    }
3428
3429    /// Remove all entries.
3430    pub fn clear(&self) -> Result<(), AgentRuntimeError> {
3431        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::clear");
3432        inner.entries.clear();
3433        inner.expected_dim = None;
3434        Ok(())
3435    }
3436
3437    /// Count entries that contain `tag` (case-sensitive).
3438    pub fn count_by_tag(&self, tag: &str) -> Result<usize, AgentRuntimeError> {
3439        let inner = recover_lock(self.inner.lock(), "SemanticStore::count_by_tag");
3440        Ok(inner
3441            .entries
3442            .iter()
3443            .filter(|e| e.tags.iter().any(|t| t.as_str() == tag))
3444            .count())
3445    }
3446
3447    /// Return all unique tags present across all stored entries, in sorted order.
3448    pub fn list_tags(&self) -> Result<Vec<String>, AgentRuntimeError> {
3449        let inner = recover_lock(self.inner.lock(), "SemanticStore::list_tags");
3450        let mut tags: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
3451        for entry in &inner.entries {
3452            for tag in &entry.tags {
3453                tags.insert(tag.clone());
3454            }
3455        }
3456        Ok(tags.into_iter().collect())
3457    }
3458
3459    /// Remove all entries that have the given tag.
3460    ///
3461    /// Returns the number of entries removed.
3462    pub fn remove_entries_with_tag(&self, tag: &str) -> Result<usize, AgentRuntimeError> {
3463        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::remove_entries_with_tag");
3464        let before = inner.entries.len();
3465        inner.entries.retain(|e| !e.tags.iter().any(|t| t == tag));
3466        Ok(before - inner.entries.len())
3467    }
3468
3469    /// Return the tag that appears most often across all entries.
3470    ///
3471    /// Returns `None` if there are no tagged entries.
3472    pub fn most_common_tag(&self) -> Result<Option<String>, AgentRuntimeError> {
3473        let inner = recover_lock(self.inner.lock(), "SemanticStore::most_common_tag");
3474        let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
3475        for entry in &inner.entries {
3476            for tag in &entry.tags {
3477                *counts.entry(tag.as_str()).or_insert(0) += 1;
3478            }
3479        }
3480        Ok(counts.into_iter().max_by_key(|(_, c)| *c).map(|(t, _)| t.to_string()))
3481    }
3482
3483    /// Return the keys of all entries that have the given tag.
3484    pub fn keys_for_tag(&self, tag: &str) -> Result<Vec<String>, AgentRuntimeError> {
3485        let inner = recover_lock(self.inner.lock(), "SemanticStore::keys_for_tag");
3486        let keys = inner
3487            .entries
3488            .iter()
3489            .filter(|e| e.tags.iter().any(|t| t == tag))
3490            .map(|e| e.key.clone())
3491            .collect();
3492        Ok(keys)
3493    }
3494
3495    /// Return a sorted list of every distinct tag appearing across all entries.
3496    pub fn unique_tags(&self) -> Result<Vec<String>, AgentRuntimeError> {
3497        let inner = recover_lock(self.inner.lock(), "SemanticStore::unique_tags");
3498        let mut tags: Vec<String> = inner
3499            .entries
3500            .iter()
3501            .flat_map(|e| e.tags.iter().cloned())
3502            .collect::<std::collections::HashSet<_>>()
3503            .into_iter()
3504            .collect();
3505        tags.sort_unstable();
3506        Ok(tags)
3507    }
3508
3509    /// Return the number of distinct tags across all stored entries.
3510    ///
3511    /// Equivalent to `list_tags()?.len()` but avoids allocating the full tag list.
3512    pub fn tag_count(&self) -> Result<usize, AgentRuntimeError> {
3513        let inner = recover_lock(self.inner.lock(), "SemanticStore::tag_count");
3514        let distinct: std::collections::HashSet<&str> = inner
3515            .entries
3516            .iter()
3517            .flat_map(|e| e.tags.iter().map(|t| t.as_str()))
3518            .collect();
3519        Ok(distinct.len())
3520    }
3521
3522    /// Return the number of entries that have an associated embedding vector.
3523    pub fn entry_count_with_embedding(&self) -> Result<usize, AgentRuntimeError> {
3524        let inner = recover_lock(self.inner.lock(), "SemanticStore::entry_count_with_embedding");
3525        Ok(inner.entries.iter().filter(|e| e.embedding.is_some()).count())
3526    }
3527
3528    /// Return the stored value for the first entry whose key matches `key`,
3529    /// or `None` if no such entry exists.
3530    ///
3531    /// Simpler alternative to [`retrieve_by_key`] when only the value is needed
3532    /// and tags are not required.
3533    ///
3534    /// [`retrieve_by_key`]: SemanticStore::retrieve_by_key
3535    pub fn get_value(&self, key: &str) -> Result<Option<String>, AgentRuntimeError> {
3536        let inner = recover_lock(self.inner.lock(), "SemanticStore::get_value");
3537        Ok(inner
3538            .entries
3539            .iter()
3540            .find(|e| e.key == key)
3541            .map(|e| e.value.clone()))
3542    }
3543
3544    /// Return the tags for the first entry whose key matches `key`.
3545    ///
3546    /// Returns `None` if no entry with that key exists.
3547    pub fn get_tags(&self, key: &str) -> Result<Option<Vec<String>>, AgentRuntimeError> {
3548        let inner = recover_lock(self.inner.lock(), "SemanticStore::get_tags");
3549        Ok(inner.entries.iter().find(|e| e.key == key).map(|e| e.tags.clone()))
3550    }
3551
3552    /// Return all entry keys that contain `tag` (case-sensitive).
3553    pub fn keys_with_tag(&self, tag: &str) -> Result<Vec<String>, AgentRuntimeError> {
3554        let inner = recover_lock(self.inner.lock(), "SemanticStore::keys_with_tag");
3555        Ok(inner
3556            .entries
3557            .iter()
3558            .filter(|e| e.tags.iter().any(|t| t.as_str() == tag))
3559            .map(|e| e.key.clone())
3560            .collect())
3561    }
3562
3563    /// Return the tags associated with the first entry matching `key`, or
3564    /// `None` if no entry with that key exists.
3565    pub fn tags_for(&self, key: &str) -> Result<Option<Vec<String>>, AgentRuntimeError> {
3566        let inner = recover_lock(self.inner.lock(), "SemanticStore::tags_for");
3567        Ok(inner
3568            .entries
3569            .iter()
3570            .find(|e| e.key == key)
3571            .map(|e| e.tags.clone()))
3572    }
3573
3574    /// Return `true` if at least one entry with the given `key` exists.
3575    pub fn has_key(&self, key: &str) -> Result<bool, AgentRuntimeError> {
3576        let inner = recover_lock(self.inner.lock(), "SemanticStore::has_key");
3577        Ok(inner.entries.iter().any(|e| e.key == key))
3578    }
3579
3580    /// Return the value string of the first entry whose key matches `key`,
3581    /// or `None` if no such entry exists.
3582    pub fn value_for(&self, key: &str) -> Result<Option<String>, AgentRuntimeError> {
3583        let inner = recover_lock(self.inner.lock(), "SemanticStore::value_for");
3584        Ok(inner
3585            .entries
3586            .iter()
3587            .find(|e| e.key == key)
3588            .map(|e| e.value.clone()))
3589    }
3590
3591    /// Return the number of entries that have no tags.
3592    pub fn entries_without_tags(&self) -> Result<usize, AgentRuntimeError> {
3593        let inner = recover_lock(self.inner.lock(), "SemanticStore::entries_without_tags");
3594        Ok(inner.entries.iter().filter(|e| e.tags.is_empty()).count())
3595    }
3596
3597    /// Return the keys of all entries that have no tags.
3598    /// Return the key of the entry with the most tags, or `None` if the store
3599    /// is empty.
3600    pub fn most_tagged_key(&self) -> Result<Option<String>, AgentRuntimeError> {
3601        let inner = recover_lock(self.inner.lock(), "SemanticStore::most_tagged_key");
3602        Ok(inner
3603            .entries
3604            .iter()
3605            .max_by_key(|e| e.tags.len())
3606            .map(|e| e.key.clone()))
3607    }
3608
3609    /// Return the count of entries whose value contains `substring`.
3610    /// Rename `old_tag` to `new_tag` across all entries.
3611    ///
3612    /// Returns the number of entries that were modified.
3613    pub fn rename_tag(&self, old_tag: &str, new_tag: &str) -> Result<usize, AgentRuntimeError> {
3614        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::rename_tag");
3615        let mut count = 0;
3616        for entry in &mut inner.entries {
3617            for tag in &mut entry.tags {
3618                if tag == old_tag {
3619                    *tag = new_tag.to_string();
3620                    count += 1;
3621                }
3622            }
3623        }
3624        Ok(count)
3625    }
3626
3627    /// Return the count of entries whose value contains `substring`.
3628    pub fn count_matching_value(&self, substring: &str) -> Result<usize, AgentRuntimeError> {
3629        let inner = recover_lock(self.inner.lock(), "SemanticStore::count_matching_value");
3630        Ok(inner.entries.iter().filter(|e| e.value.contains(substring)).count())
3631    }
3632
3633    /// Return the keys of all entries that have no tags.
3634    pub fn entries_with_no_tags(&self) -> Result<Vec<String>, AgentRuntimeError> {
3635        let inner = recover_lock(self.inner.lock(), "SemanticStore::entries_with_no_tags");
3636        Ok(inner
3637            .entries
3638            .iter()
3639            .filter(|e| e.tags.is_empty())
3640            .map(|e| e.key.clone())
3641            .collect())
3642    }
3643
3644    /// Return the mean number of tags per entry.
3645    ///
3646    /// Returns `0.0` when the store is empty.
3647    pub fn avg_tag_count_per_entry(&self) -> Result<f64, AgentRuntimeError> {
3648        let inner = recover_lock(self.inner.lock(), "SemanticStore::avg_tag_count_per_entry");
3649        let n = inner.entries.len();
3650        if n == 0 {
3651            return Ok(0.0);
3652        }
3653        let total: usize = inner.entries.iter().map(|e| e.tags.len()).sum();
3654        Ok(total as f64 / n as f64)
3655    }
3656
3657    /// Return the key of the most recently inserted entry, or `None` if empty.
3658    pub fn most_recent_key(&self) -> Result<Option<String>, AgentRuntimeError> {
3659        let inner = recover_lock(self.inner.lock(), "SemanticStore::most_recent_key");
3660        Ok(inner.entries.last().map(|e| e.key.clone()))
3661    }
3662
3663    /// Return the key of the earliest inserted entry, or `None` if empty.
3664    pub fn oldest_key(&self) -> Result<Option<String>, AgentRuntimeError> {
3665        let inner = recover_lock(self.inner.lock(), "SemanticStore::oldest_key");
3666        Ok(inner.entries.first().map(|e| e.key.clone()))
3667    }
3668
3669    /// Remove all entries whose key equals `key`.
3670    ///
3671    /// Returns the number of entries removed.
3672    pub fn remove_by_key(&self, key: &str) -> Result<usize, AgentRuntimeError> {
3673        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::remove_by_key");
3674        let before = inner.entries.len();
3675        inner.entries.retain(|e| e.key != key);
3676        Ok(before - inner.entries.len())
3677    }
3678
3679    /// Return the number of entries that include `tag` in their tag list.
3680    pub fn entry_count_with_tag(&self, tag: &str) -> Result<usize, AgentRuntimeError> {
3681        let inner = recover_lock(self.inner.lock(), "SemanticStore::entry_count_with_tag");
3682        Ok(inner
3683            .entries
3684            .iter()
3685            .filter(|e| e.tags.iter().any(|t| t == tag))
3686            .count())
3687    }
3688
3689    /// Return all stored entry keys in insertion order.
3690    ///
3691    /// Duplicate keys (if any) will appear multiple times.
3692    pub fn list_keys(&self) -> Result<Vec<String>, AgentRuntimeError> {
3693        let inner = recover_lock(self.inner.lock(), "SemanticStore::list_keys");
3694        Ok(inner.entries.iter().map(|e| e.key.clone()).collect())
3695    }
3696
3697    /// Return all stored entry keys in insertion order.
3698    ///
3699    /// Alias for [`list_keys`] using more idiomatic naming.
3700    ///
3701    /// [`list_keys`]: SemanticStore::list_keys
3702    pub fn keys(&self) -> Result<Vec<String>, AgentRuntimeError> {
3703        self.list_keys()
3704    }
3705
3706    /// Return the stored value for every entry, in insertion order.
3707    pub fn values(&self) -> Result<Vec<String>, AgentRuntimeError> {
3708        let inner = recover_lock(self.inner.lock(), "SemanticStore::values");
3709        Ok(inner.entries.iter().map(|e| e.value.clone()).collect())
3710    }
3711
3712    /// Return all keys that contain `pattern` as a substring (case-insensitive).
3713    ///
3714    /// Useful for prefix/contains searches without loading values.
3715    pub fn keys_matching(&self, pattern: &str) -> Result<Vec<String>, AgentRuntimeError> {
3716        let inner = recover_lock(self.inner.lock(), "SemanticStore::keys_matching");
3717        let lower = pattern.to_ascii_lowercase();
3718        Ok(inner
3719            .entries
3720            .iter()
3721            .filter(|e| e.key.to_ascii_lowercase().contains(&lower))
3722            .map(|e| e.key.clone())
3723            .collect())
3724    }
3725
3726    /// Update the stored value of the first entry whose key matches `key`.
3727    ///
3728    /// Returns `Ok(true)` if found and updated, `Ok(false)` if not found.
3729    pub fn update_value(
3730        &self,
3731        key: &str,
3732        new_value: impl Into<String>,
3733    ) -> Result<bool, AgentRuntimeError> {
3734        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::update_value");
3735        if let Some(entry) = inner.entries.iter_mut().find(|e| e.key == key) {
3736            entry.value = new_value.into();
3737            Ok(true)
3738        } else {
3739            Ok(false)
3740        }
3741    }
3742
3743    /// Return all stored entries as a `HashMap<key, value>`.
3744    ///
3745    /// Useful for serialization or bulk inspection.  Tags and embeddings are
3746    /// discarded; use `iter()` / `retrieve_by_key` when those are needed.
3747    pub fn to_map(&self) -> Result<std::collections::HashMap<String, String>, AgentRuntimeError> {
3748        let inner = recover_lock(self.inner.lock(), "SemanticStore::to_map");
3749        Ok(inner
3750            .entries
3751            .iter()
3752            .map(|e| (e.key.clone(), e.value.clone()))
3753            .collect())
3754    }
3755
3756    /// Update the tags of the first entry whose key matches `key`.
3757    ///
3758    /// Returns `Ok(true)` if found and updated, `Ok(false)` if not found.
3759    pub fn update_tags(
3760        &self,
3761        key: &str,
3762        new_tags: Vec<String>,
3763    ) -> Result<bool, AgentRuntimeError> {
3764        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::update_tags");
3765        if let Some(entry) = inner.entries.iter_mut().find(|e| e.key == key) {
3766            entry.tags = new_tags;
3767            Ok(true)
3768        } else {
3769            Ok(false)
3770        }
3771    }
3772
3773    /// Return the sum of byte lengths of all stored values.
3774    ///
3775    /// Returns `0` if no entries have been stored.
3776    pub fn total_value_bytes(&self) -> Result<usize, AgentRuntimeError> {
3777        let inner = recover_lock(self.inner.lock(), "SemanticStore::total_value_bytes");
3778        Ok(inner.entries.iter().map(|e| e.value.len()).sum())
3779    }
3780
3781    /// Return the mean byte length of all stored values.
3782    ///
3783    /// Returns `0.0` if no entries have been stored.
3784    pub fn avg_value_bytes(&self) -> Result<f64, AgentRuntimeError> {
3785        let inner = recover_lock(self.inner.lock(), "SemanticStore::avg_value_bytes");
3786        if inner.entries.is_empty() {
3787            return Ok(0.0);
3788        }
3789        let total: usize = inner.entries.iter().map(|e| e.value.len()).sum();
3790        Ok(total as f64 / inner.entries.len() as f64)
3791    }
3792
3793    /// Return the byte length of the longest stored value.
3794    ///
3795    /// Returns `0` if no entries have been stored.
3796    pub fn max_value_bytes(&self) -> Result<usize, AgentRuntimeError> {
3797        let inner = recover_lock(self.inner.lock(), "SemanticStore::max_value_bytes");
3798        Ok(inner.entries.iter().map(|e| e.value.len()).max().unwrap_or(0))
3799    }
3800
3801    /// Return the byte length of the shortest stored value.
3802    ///
3803    /// Returns `0` if no entries have been stored.
3804    pub fn min_value_bytes(&self) -> Result<usize, AgentRuntimeError> {
3805        let inner = recover_lock(self.inner.lock(), "SemanticStore::min_value_bytes");
3806        Ok(inner.entries.iter().map(|e| e.value.len()).min().unwrap_or(0))
3807    }
3808
3809    /// Return all stored keys in ascending sorted order.
3810    pub fn all_keys(&self) -> Result<Vec<String>, AgentRuntimeError> {
3811        let inner = recover_lock(self.inner.lock(), "SemanticStore::all_keys");
3812        let mut keys: Vec<String> = inner.entries.iter().map(|e| e.key.clone()).collect();
3813        keys.sort_unstable();
3814        Ok(keys)
3815    }
3816
3817    /// Return all stored keys whose string representation starts with `prefix`,
3818    /// in ascending sorted order.
3819    ///
3820    /// Returns an empty `Vec` if no keys share the prefix.
3821    pub fn keys_with_prefix(&self, prefix: &str) -> Result<Vec<String>, AgentRuntimeError> {
3822        let inner = recover_lock(self.inner.lock(), "SemanticStore::keys_with_prefix");
3823        let mut keys: Vec<String> = inner
3824            .entries
3825            .iter()
3826            .filter(|e| e.key.starts_with(prefix))
3827            .map(|e| e.key.clone())
3828            .collect();
3829        keys.sort_unstable();
3830        Ok(keys)
3831    }
3832
3833    /// Return the total number of stored entries.
3834    pub fn len(&self) -> Result<usize, AgentRuntimeError> {
3835        let inner = recover_lock(self.inner.lock(), "SemanticStore::len");
3836        Ok(inner.entries.len())
3837    }
3838
3839    /// Return `true` if no entries have been stored.
3840    pub fn is_empty(&self) -> Result<bool, AgentRuntimeError> {
3841        Ok(self.len()? == 0)
3842    }
3843
3844    /// Return the number of stored entries.
3845    ///
3846    /// Alias for [`len`] using conventional naming.
3847    ///
3848    /// [`len`]: SemanticStore::len
3849    pub fn count(&self) -> Result<usize, AgentRuntimeError> {
3850        self.len()
3851    }
3852
3853    /// Remove the first entry with key `key`.
3854    ///
3855    /// Returns `Ok(true)` if an entry was found and removed, `Ok(false)` if
3856    /// no entry with that key exists.
3857    pub fn remove(&self, key: &str) -> Result<bool, AgentRuntimeError> {
3858        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::remove");
3859        let before = inner.entries.len();
3860        inner.entries.retain(|e| e.key != key);
3861        Ok(inner.entries.len() < before)
3862    }
3863
3864}
3865
3866impl Default for SemanticStore {
3867    fn default() -> Self {
3868        Self::new()
3869    }
3870}
3871
3872// ── WorkingMemory ─────────────────────────────────────────────────────────────
3873
3874/// A bounded, key-value working memory for transient agent state.
3875///
3876/// When capacity is exceeded, the oldest entry (by insertion order) is evicted.
3877///
3878/// ## Guarantees
3879/// - Thread-safe via `Arc<Mutex<_>>`
3880/// - Bounded: never exceeds `capacity` entries
3881/// - Deterministic eviction: LRU (oldest insertion first)
3882#[derive(Debug, Clone)]
3883pub struct WorkingMemory {
3884    capacity: usize,
3885    inner: Arc<Mutex<WorkingInner>>,
3886}
3887
3888#[derive(Debug)]
3889struct WorkingInner {
3890    map: HashMap<String, String>,
3891    order: VecDeque<String>,
3892}
3893
3894impl WorkingMemory {
3895    /// Create a new `WorkingMemory` with the given capacity.
3896    ///
3897    /// # Returns
3898    /// - `Ok(WorkingMemory)` — on success
3899    /// - `Err(AgentRuntimeError::Memory)` — if `capacity == 0`
3900    pub fn new(capacity: usize) -> Result<Self, AgentRuntimeError> {
3901        if capacity == 0 {
3902            return Err(AgentRuntimeError::Memory(
3903                "WorkingMemory capacity must be > 0".into(),
3904            ));
3905        }
3906        Ok(Self {
3907            capacity,
3908            inner: Arc::new(Mutex::new(WorkingInner {
3909                map: HashMap::new(),
3910                order: VecDeque::new(),
3911            })),
3912        })
3913    }
3914
3915    /// Insert or update a key-value pair, evicting the oldest entry if over capacity.
3916    #[tracing::instrument(skip(self))]
3917    pub fn set(
3918        &self,
3919        key: impl Into<String> + std::fmt::Debug,
3920        value: impl Into<String> + std::fmt::Debug,
3921    ) -> Result<(), AgentRuntimeError> {
3922        let key = key.into();
3923        let value = value.into();
3924        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::set");
3925
3926        // Remove existing key from order tracking if present
3927        if inner.map.contains_key(&key) {
3928            inner.order.retain(|k| k != &key);
3929        } else if inner.map.len() >= self.capacity {
3930            // Evict oldest
3931            if let Some(oldest) = inner.order.pop_front() {
3932                inner.map.remove(&oldest);
3933            }
3934        }
3935
3936        inner.order.push_back(key.clone());
3937        inner.map.insert(key, value);
3938        Ok(())
3939    }
3940
3941    /// Retrieve a value by key.
3942    ///
3943    /// # Returns
3944    /// - `Some(value)` — if the key exists
3945    /// - `None` — if not found
3946    #[tracing::instrument(skip(self))]
3947    pub fn get(&self, key: &str) -> Result<Option<String>, AgentRuntimeError> {
3948        let inner = recover_lock(self.inner.lock(), "WorkingMemory::get");
3949        Ok(inner.map.get(key).cloned())
3950    }
3951
3952    /// Insert multiple key-value pairs with a single lock acquisition.
3953    ///
3954    /// Each entry follows the same eviction semantics as [`set`]: if the key
3955    /// already exists it is updated in-place; if inserting a new key would
3956    /// exceed capacity, the oldest key is evicted first.
3957    ///
3958    /// [`set`]: WorkingMemory::set
3959    pub fn set_many(
3960        &self,
3961        pairs: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
3962    ) -> Result<(), AgentRuntimeError> {
3963        let capacity = self.capacity;
3964        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::set_many");
3965        for (key, value) in pairs {
3966            let key: String = key.into();
3967            let value: String = value.into();
3968            if inner.map.contains_key(&key) {
3969                inner.order.retain(|k| k != &key);
3970            } else if inner.map.len() >= capacity {
3971                if let Some(oldest) = inner.order.pop_front() {
3972                    inner.map.remove(&oldest);
3973                }
3974            }
3975            inner.order.push_back(key.clone());
3976            inner.map.insert(key, value);
3977        }
3978        Ok(())
3979    }
3980
3981    /// Update the value of an existing key without inserting if absent.
3982    ///
3983    /// Insert `key → value` only if `key` is not already present.
3984    ///
3985    /// Returns `Ok(true)` if the entry was inserted, `Ok(false)` if the key
3986    /// was already present (the existing value is left unchanged).  Capacity
3987    /// limits and LRU eviction apply as with [`set`].
3988    ///
3989    /// [`set`]: WorkingMemory::set
3990    pub fn set_if_absent(
3991        &self,
3992        key: impl Into<String> + std::fmt::Debug,
3993        value: impl Into<String> + std::fmt::Debug,
3994    ) -> Result<bool, AgentRuntimeError> {
3995        let key = key.into();
3996        {
3997            let inner = recover_lock(self.inner.lock(), "WorkingMemory::set_if_absent");
3998            if inner.map.contains_key(&key) {
3999                return Ok(false);
4000            }
4001        }
4002        self.set(key, value)?;
4003        Ok(true)
4004    }
4005
4006    /// Returns `Ok(true)` if the key existed and was updated, `Ok(false)` if not present.
4007    pub fn update_if_exists(
4008        &self,
4009        key: &str,
4010        value: impl Into<String>,
4011    ) -> Result<bool, AgentRuntimeError> {
4012        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::update_if_exists");
4013        if let Some(v) = inner.map.get_mut(key) {
4014            *v = value.into();
4015            Ok(true)
4016        } else {
4017            Ok(false)
4018        }
4019    }
4020
4021    /// Update multiple existing keys in a single lock acquisition.
4022    ///
4023    /// Each `(key, value)` pair is applied only if the key is already present
4024    /// (same semantics as [`update_if_exists`]).  Returns the number of keys
4025    /// that were found and updated.
4026    ///
4027    /// [`update_if_exists`]: WorkingMemory::update_if_exists
4028    pub fn update_many(
4029        &self,
4030        pairs: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
4031    ) -> Result<usize, AgentRuntimeError> {
4032        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::update_many");
4033        let mut updated = 0;
4034        for (key, value) in pairs {
4035            let key: String = key.into();
4036            if let Some(v) = inner.map.get_mut(&key) {
4037                *v = value.into();
4038                updated += 1;
4039            }
4040        }
4041        Ok(updated)
4042    }
4043
4044    /// Return all keys whose names start with `prefix`, in insertion order.
4045    ///
4046    /// Useful for namespace-prefixed keys such as `"user:name"`,
4047    /// `"user:email"` where all user-related keys share the `"user:"` prefix.
4048    pub fn keys_starting_with(&self, prefix: &str) -> Result<Vec<String>, AgentRuntimeError> {
4049        let inner = recover_lock(self.inner.lock(), "WorkingMemory::keys_starting_with");
4050        Ok(inner
4051            .map
4052            .keys()
4053            .filter(|k| k.starts_with(prefix))
4054            .cloned()
4055            .collect())
4056    }
4057
4058    /// Return all `(key, value)` pairs whose value contains `pattern` as a
4059    /// substring.  Comparison is case-sensitive.
4060    ///
4061    /// Useful for scanning working memory for values that match a keyword
4062    /// without iterating the full map externally.
4063    pub fn values_matching(&self, pattern: &str) -> Result<Vec<(String, String)>, AgentRuntimeError> {
4064        let inner = recover_lock(self.inner.lock(), "WorkingMemory::values_matching");
4065        Ok(inner
4066            .map
4067            .iter()
4068            .filter(|(_, v)| v.contains(pattern))
4069            .map(|(k, v)| (k.clone(), v.clone()))
4070            .collect())
4071    }
4072
4073    /// Return the byte length of the value stored at `key`, or `None` if the
4074    /// key is not present.
4075    ///
4076    /// Useful for estimating memory usage without cloning the value string.
4077    pub fn value_length(&self, key: &str) -> Result<Option<usize>, AgentRuntimeError> {
4078        let inner = recover_lock(self.inner.lock(), "WorkingMemory::value_length");
4079        Ok(inner.map.get(key).map(|v| v.len()))
4080    }
4081
4082    /// Rename a key without changing its value or insertion order.
4083    ///
4084    /// Return `true` if **all** of the given keys are currently stored.
4085    ///
4086    /// An empty iterator returns `true` vacuously.
4087    pub fn contains_all<'a>(&self, keys: impl IntoIterator<Item = &'a str>) -> Result<bool, AgentRuntimeError> {
4088        let inner = recover_lock(self.inner.lock(), "WorkingMemory::contains_all");
4089        Ok(keys.into_iter().all(|k| inner.map.contains_key(k)))
4090    }
4091
4092    /// Return `true` if **any** of the given keys is currently stored.
4093    ///
4094    /// An empty iterator returns `false`.
4095    pub fn has_any_key<'a>(&self, keys: impl IntoIterator<Item = &'a str>) -> Result<bool, AgentRuntimeError> {
4096        let inner = recover_lock(self.inner.lock(), "WorkingMemory::has_any_key");
4097        Ok(keys.into_iter().any(|k| inner.map.contains_key(k)))
4098    }
4099
4100    /// Returns `true` if `old_key` existed and was renamed.  Returns `false` if
4101    /// `old_key` is not present.  If `new_key` already exists it is overwritten
4102    /// and its old value is lost.
4103    pub fn rename(
4104        &self,
4105        old_key: &str,
4106        new_key: impl Into<String>,
4107    ) -> Result<bool, AgentRuntimeError> {
4108        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::rename");
4109        let value = match inner.map.remove(old_key) {
4110            None => return Ok(false),
4111            Some(v) => v,
4112        };
4113        let new_key = new_key.into();
4114        // Update the insertion-order list.
4115        if let Some(pos) = inner.order.iter().position(|k| k == old_key) {
4116            inner.order[pos] = new_key.clone();
4117        }
4118        inner.map.insert(new_key, value);
4119        Ok(true)
4120    }
4121
4122    /// Retrieve multiple values in a single lock acquisition.
4123    ///
4124    /// Returns a `Vec` of the same length as `keys`.  Each entry is `Some(value)`
4125    /// if the key is present, `None` if not.
4126    pub fn get_many(&self, keys: &[&str]) -> Result<Vec<Option<String>>, AgentRuntimeError> {
4127        let inner = recover_lock(self.inner.lock(), "WorkingMemory::get_many");
4128        Ok(keys.iter().map(|k| inner.map.get(*k).cloned()).collect())
4129    }
4130
4131    /// Return `true` if a value is stored under `key`.
4132    pub fn contains(&self, key: &str) -> Result<bool, AgentRuntimeError> {
4133        let inner = recover_lock(self.inner.lock(), "WorkingMemory::contains");
4134        Ok(inner.map.contains_key(key))
4135    }
4136
4137    /// Return the value associated with `key`, or `default` if not set.
4138    pub fn get_or_default(
4139        &self,
4140        key: &str,
4141        default: impl Into<String>,
4142    ) -> Result<String, AgentRuntimeError> {
4143        Ok(self.get(key)?.unwrap_or_else(|| default.into()))
4144    }
4145
4146    /// Remove a single entry by key.  Returns `true` if the key existed.
4147    pub fn remove(&self, key: &str) -> Result<bool, AgentRuntimeError> {
4148        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::remove");
4149        if inner.map.remove(key).is_some() {
4150            inner.order.retain(|k| k != key);
4151            Ok(true)
4152        } else {
4153            Ok(false)
4154        }
4155    }
4156
4157    /// Return all keys in insertion order.
4158    pub fn keys(&self) -> Result<Vec<String>, AgentRuntimeError> {
4159        let inner = recover_lock(self.inner.lock(), "WorkingMemory::keys");
4160        Ok(inner.order.iter().cloned().collect())
4161    }
4162
4163    /// Return all values in insertion order (parallel to [`keys`]).
4164    ///
4165    /// [`keys`]: WorkingMemory::keys
4166    pub fn values(&self) -> Result<Vec<String>, AgentRuntimeError> {
4167        let inner = recover_lock(self.inner.lock(), "WorkingMemory::values");
4168        Ok(inner
4169            .order
4170            .iter()
4171            .filter_map(|k| inner.map.get(k).cloned())
4172            .collect())
4173    }
4174
4175    /// Remove all entries from working memory.
4176    pub fn clear(&self) -> Result<(), AgentRuntimeError> {
4177        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::clear");
4178        inner.map.clear();
4179        inner.order.clear();
4180        Ok(())
4181    }
4182
4183    /// Remove all entries and return them as a `Vec<(key, value)>` in insertion order.
4184    ///
4185    /// Return all entries as `(key, value)` pairs sorted alphabetically by key.
4186    ///
4187    /// Unlike [`iter`]/[`entries`] which return entries in insertion order,
4188    /// this always returns a deterministically ordered snapshot.
4189    ///
4190    /// [`iter`]: WorkingMemory::iter
4191    /// [`entries`]: WorkingMemory::entries
4192    pub fn iter_sorted(&self) -> Result<Vec<(String, String)>, AgentRuntimeError> {
4193        let inner = recover_lock(self.inner.lock(), "WorkingMemory::iter_sorted");
4194        let mut pairs: Vec<(String, String)> = inner
4195            .map
4196            .iter()
4197            .map(|(k, v)| (k.clone(), v.clone()))
4198            .collect();
4199        pairs.sort_unstable_by(|a, b| a.0.cmp(&b.0));
4200        Ok(pairs)
4201    }
4202
4203    /// After this call the memory is empty. Useful for atomically moving the
4204    /// contents to another data structure.
4205    pub fn drain(&self) -> Result<Vec<(String, String)>, AgentRuntimeError> {
4206        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::drain");
4207        let pairs: Vec<(String, String)> = inner
4208            .order
4209            .iter()
4210            .filter_map(|k| inner.map.get(k).map(|v| (k.clone(), v.clone())))
4211            .collect();
4212        inner.map.clear();
4213        inner.order.clear();
4214        Ok(pairs)
4215    }
4216
4217    /// Clone all current entries into a `HashMap<String, String>`.
4218    ///
4219    /// Unlike [`drain`], this leaves the working memory unchanged.
4220    ///
4221    /// [`drain`]: WorkingMemory::drain
4222    pub fn snapshot(&self) -> Result<std::collections::HashMap<String, String>, AgentRuntimeError> {
4223        let inner = recover_lock(self.inner.lock(), "WorkingMemory::snapshot");
4224        Ok(inner.map.clone())
4225    }
4226
4227    /// Return the current number of entries.
4228    pub fn len(&self) -> Result<usize, AgentRuntimeError> {
4229        let inner = recover_lock(self.inner.lock(), "WorkingMemory::len");
4230        Ok(inner.map.len())
4231    }
4232
4233    /// Return `true` if no entries are stored.
4234    pub fn is_empty(&self) -> Result<bool, AgentRuntimeError> {
4235        Ok(self.len()? == 0)
4236    }
4237
4238    /// Iterate over all key-value pairs in insertion order.
4239    ///
4240    /// Equivalent to [`entries`]; provided as a more idiomatic name
4241    /// for `for`-loop patterns.
4242    ///
4243    /// [`entries`]: WorkingMemory::entries
4244    pub fn iter(&self) -> Result<Vec<(String, String)>, AgentRuntimeError> {
4245        self.entries()
4246    }
4247
4248    /// Return all key-value pairs in insertion order.
4249    pub fn entries(&self) -> Result<Vec<(String, String)>, AgentRuntimeError> {
4250        let inner = recover_lock(self.inner.lock(), "WorkingMemory::entries");
4251        let entries = inner
4252            .order
4253            .iter()
4254            .filter_map(|k| inner.map.get(k).map(|v| (k.clone(), v.clone())))
4255            .collect();
4256        Ok(entries)
4257    }
4258
4259    /// Remove and return the oldest entry (first inserted that is still present).
4260    ///
4261    /// Returns `None` if the memory is empty.
4262    pub fn pop_oldest(&self) -> Result<Option<(String, String)>, AgentRuntimeError> {
4263        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::pop_oldest");
4264        if let Some(key) = inner.order.pop_front() {
4265            let value = inner.map.remove(&key).unwrap_or_default();
4266            Ok(Some((key, value)))
4267        } else {
4268            Ok(None)
4269        }
4270    }
4271
4272    /// Peek at the oldest entry without removing it.
4273    ///
4274    /// Returns `None` if the memory is empty.  Unlike [`pop_oldest`] this
4275    /// method does not modify the store.
4276    ///
4277    /// [`pop_oldest`]: WorkingMemory::pop_oldest
4278    pub fn peek_oldest(&self) -> Result<Option<(String, String)>, AgentRuntimeError> {
4279        let inner = recover_lock(self.inner.lock(), "WorkingMemory::peek_oldest");
4280        Ok(inner.order.front().and_then(|key| {
4281            inner.map.get(key).map(|val| (key.clone(), val.clone()))
4282        }))
4283    }
4284
4285    /// Return the maximum number of entries this store can hold.
4286    ///
4287    /// When [`len`] reaches this value, the oldest entry is evicted on the
4288    /// next [`set`] call for a new key.
4289    ///
4290    /// [`len`]: WorkingMemory::len
4291    /// [`set`]: WorkingMemory::set
4292    pub fn capacity(&self) -> usize {
4293        self.capacity
4294    }
4295
4296    /// Return the current fill ratio as `len / capacity`.
4297    ///
4298    /// Returns a value in `[0.0, 1.0]`.  `1.0` means the memory is full and
4299    /// the next insert will evict the oldest entry.
4300    pub fn fill_ratio(&self) -> Result<f64, AgentRuntimeError> {
4301        Ok(self.len()? as f64 / self.capacity as f64)
4302    }
4303
4304    /// Return `true` when the number of stored entries equals the configured capacity.
4305    ///
4306    /// When `true`, the next [`set`] call for a new key will evict the oldest entry.
4307    ///
4308    /// [`set`]: WorkingMemory::set
4309    pub fn is_at_capacity(&self) -> Result<bool, AgentRuntimeError> {
4310        Ok(self.len()? >= self.capacity)
4311    }
4312
4313    /// Remove all entries whose key begins with `prefix`.
4314    ///
4315    /// Returns the number of entries removed.  Removal preserves insertion order
4316    /// for the surviving entries.
4317    pub fn remove_keys_starting_with(&self, prefix: &str) -> Result<usize, AgentRuntimeError> {
4318        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::remove_keys_starting_with");
4319        let before = inner.map.len();
4320        inner.map.retain(|k, _| !k.starts_with(prefix));
4321        Ok(before - inner.map.len())
4322    }
4323
4324    /// Return the total number of bytes used by all stored values.
4325    ///
4326    /// Useful for estimating memory pressure without allocating a full snapshot.
4327    pub fn total_value_bytes(&self) -> Result<usize, AgentRuntimeError> {
4328        let inner = recover_lock(self.inner.lock(), "WorkingMemory::total_value_bytes");
4329        Ok(inner.map.values().map(|v| v.len()).sum())
4330    }
4331
4332    /// Return the byte length of the longest key currently stored.
4333    ///
4334    /// Returns `0` when the memory is empty.
4335    pub fn max_key_length(&self) -> Result<usize, AgentRuntimeError> {
4336        let inner = recover_lock(self.inner.lock(), "WorkingMemory::max_key_length");
4337        Ok(inner.map.keys().map(|k| k.len()).max().unwrap_or(0))
4338    }
4339
4340    /// Return the byte length of the longest stored value.
4341    ///
4342    /// Returns `0` when the memory is empty.
4343    pub fn max_value_length(&self) -> Result<usize, AgentRuntimeError> {
4344        let inner = recover_lock(self.inner.lock(), "WorkingMemory::max_value_length");
4345        Ok(inner.map.values().map(|v| v.len()).max().unwrap_or(0))
4346    }
4347
4348    /// Return the byte length of the shortest stored value.
4349    ///
4350    /// Returns `0` when the memory is empty.
4351    pub fn min_value_length(&self) -> Result<usize, AgentRuntimeError> {
4352        let inner = recover_lock(self.inner.lock(), "WorkingMemory::min_value_length");
4353        Ok(inner.map.values().map(|v| v.len()).min().unwrap_or(0))
4354    }
4355
4356    /// Return the number of keys whose text contains `substring`.
4357    ///
4358    /// The search is case-sensitive.  Returns `0` when the store is empty or
4359    /// no key matches.
4360    pub fn key_count_matching(&self, substring: &str) -> Result<usize, AgentRuntimeError> {
4361        let inner = recover_lock(self.inner.lock(), "WorkingMemory::key_count_matching");
4362        Ok(inner.map.keys().filter(|k| k.contains(substring)).count())
4363    }
4364
4365    /// Return the mean byte length of all stored values.
4366    ///
4367    /// Returns `0.0` when the store is empty.
4368    pub fn avg_value_length(&self) -> Result<f64, AgentRuntimeError> {
4369        let inner = recover_lock(self.inner.lock(), "WorkingMemory::avg_value_length");
4370        let n = inner.map.len();
4371        if n == 0 {
4372            return Ok(0.0);
4373        }
4374        let total: usize = inner.map.values().map(|v| v.len()).sum();
4375        Ok(total as f64 / n as f64)
4376    }
4377
4378    /// Return the number of entries whose value exceeds `min_bytes` bytes in length.
4379    pub fn count_above_value_length(&self, min_bytes: usize) -> Result<usize, AgentRuntimeError> {
4380        let inner = recover_lock(self.inner.lock(), "WorkingMemory::count_above_value_length");
4381        Ok(inner.map.values().filter(|v| v.len() > min_bytes).count())
4382    }
4383
4384    /// Return the key with the most bytes, or `None` if the store is empty.
4385    ///
4386    /// When multiple keys share the maximum byte length, one of them is
4387    /// returned (unspecified which).
4388    pub fn longest_key(&self) -> Result<Option<String>, AgentRuntimeError> {
4389        let inner = recover_lock(self.inner.lock(), "WorkingMemory::longest_key");
4390        Ok(inner
4391            .map
4392            .keys()
4393            .max_by_key(|k| k.len())
4394            .map(|k| k.clone()))
4395    }
4396
4397    /// Return the value with the most bytes, or `None` if the store is empty.
4398    ///
4399    /// When multiple values share the maximum byte length, one of them is
4400    /// returned (unspecified which).
4401    pub fn longest_value(&self) -> Result<Option<String>, AgentRuntimeError> {
4402        let inner = recover_lock(self.inner.lock(), "WorkingMemory::longest_value");
4403        Ok(inner
4404            .map
4405            .values()
4406            .max_by_key(|v| v.len())
4407            .map(|v| v.clone()))
4408    }
4409
4410    /// Return all `(key, value)` pairs sorted alphabetically by key.
4411    ///
4412    /// Provides a stable, deterministic view of the store contents.
4413    /// Returns an empty `Vec` for an empty store.
4414    pub fn key_value_pairs_sorted(&self) -> Result<Vec<(String, String)>, AgentRuntimeError> {
4415        let inner = recover_lock(self.inner.lock(), "WorkingMemory::key_value_pairs_sorted");
4416        let mut pairs: Vec<(String, String)> = inner
4417            .map
4418            .iter()
4419            .map(|(k, v)| (k.clone(), v.clone()))
4420            .collect();
4421        pairs.sort_unstable_by(|a, b| a.0.cmp(&b.0));
4422        Ok(pairs)
4423    }
4424
4425    /// Return all `(key, value)` pairs whose key starts with `prefix`.
4426    pub fn pairs_starting_with(&self, prefix: &str) -> Result<Vec<(String, String)>, AgentRuntimeError> {
4427        let inner = recover_lock(self.inner.lock(), "WorkingMemory::pairs_starting_with");
4428        let pairs = inner
4429            .map
4430            .iter()
4431            .filter(|(k, _)| k.starts_with(prefix))
4432            .map(|(k, v)| (k.clone(), v.clone()))
4433            .collect();
4434        Ok(pairs)
4435    }
4436
4437    /// Return the sum of byte lengths of all stored keys.
4438    pub fn total_key_bytes(&self) -> Result<usize, AgentRuntimeError> {
4439        let inner = recover_lock(self.inner.lock(), "WorkingMemory::total_key_bytes");
4440        Ok(inner.map.keys().map(|k| k.len()).sum())
4441    }
4442
4443    /// Return all key-value pairs sorted by key byte length ascending, then
4444    /// alphabetically for equal lengths.
4445    ///
4446    /// Returns an empty `Vec` for an empty store.
4447    pub fn entries_sorted_by_key_length(
4448        &self,
4449    ) -> Result<Vec<(String, String)>, AgentRuntimeError> {
4450        let inner =
4451            recover_lock(self.inner.lock(), "WorkingMemory::entries_sorted_by_key_length");
4452        let mut pairs: Vec<(String, String)> = inner
4453            .map
4454            .iter()
4455            .map(|(k, v)| (k.clone(), v.clone()))
4456            .collect();
4457        pairs.sort_unstable_by(|(a, _), (b, _)| a.len().cmp(&b.len()).then(a.cmp(b)));
4458        Ok(pairs)
4459    }
4460
4461    /// Return the maximum byte length among all stored values.
4462    ///
4463    /// Returns `0` for an empty store.
4464    pub fn value_bytes_max(&self) -> Result<usize, AgentRuntimeError> {
4465        let inner = recover_lock(self.inner.lock(), "WorkingMemory::value_bytes_max");
4466        Ok(inner.map.values().map(|v| v.len()).max().unwrap_or(0))
4467    }
4468
4469    /// Return all keys whose string representation ends with `suffix`,
4470    /// sorted alphabetically.
4471    ///
4472    /// Returns an empty `Vec` when no key qualifies.
4473    pub fn keys_with_suffix(
4474        &self,
4475        suffix: &str,
4476    ) -> Result<Vec<String>, AgentRuntimeError> {
4477        let inner = recover_lock(self.inner.lock(), "WorkingMemory::keys_with_suffix");
4478        let mut keys: Vec<String> = inner
4479            .map
4480            .keys()
4481            .filter(|k| k.ends_with(suffix))
4482            .cloned()
4483            .collect();
4484        keys.sort_unstable();
4485        Ok(keys)
4486    }
4487
4488    /// Return the average byte length of all stored keys.
4489    ///
4490    /// Returns `0.0` for an empty store.
4491    pub fn avg_key_length(&self) -> Result<f64, AgentRuntimeError> {
4492        let inner = recover_lock(self.inner.lock(), "WorkingMemory::avg_key_length");
4493        if inner.map.is_empty() {
4494            return Ok(0.0);
4495        }
4496        let total: usize = inner.map.keys().map(|k| k.len()).sum();
4497        Ok(total as f64 / inner.map.len() as f64)
4498    }
4499
4500    /// Return the maximum key byte length, or `0` if the store is empty.
4501    ///
4502    /// Useful as a quick bound on the widest key that will be emitted.
4503    pub fn max_key_bytes(&self) -> Result<usize, AgentRuntimeError> {
4504        let inner = recover_lock(self.inner.lock(), "WorkingMemory::max_key_bytes");
4505        Ok(inner.map.keys().map(|k| k.len()).max().unwrap_or(0))
4506    }
4507
4508    /// Return the count of values whose byte length exceeds `min_bytes`.
4509    ///
4510    /// Returns `0` for an empty store or when no value exceeds the threshold.
4511    pub fn value_count_above_bytes(&self, min_bytes: usize) -> Result<usize, AgentRuntimeError> {
4512        let inner = recover_lock(self.inner.lock(), "WorkingMemory::value_count_above_bytes");
4513        Ok(inner.map.values().filter(|v| v.len() > min_bytes).count())
4514    }
4515
4516    /// Return `true` if any key in the store starts with `prefix`.
4517    ///
4518    /// Returns `false` for an empty store.
4519    pub fn has_key_starting_with(&self, prefix: &str) -> Result<bool, AgentRuntimeError> {
4520        let inner = recover_lock(self.inner.lock(), "WorkingMemory::has_key_starting_with");
4521        Ok(inner.map.keys().any(|k| k.starts_with(prefix)))
4522    }
4523
4524    /// Return the number of keys whose names begin with `prefix`.
4525    ///
4526    /// Returns `0` for an empty store or when no key matches.
4527    pub fn key_count_starting_with(&self, prefix: &str) -> Result<usize, AgentRuntimeError> {
4528        let inner = recover_lock(self.inner.lock(), "WorkingMemory::key_count_starting_with");
4529        Ok(inner.map.keys().filter(|k| k.starts_with(prefix)).count())
4530    }
4531
4532    /// Return the value with the greatest byte length in the store.
4533    ///
4534    /// Returns `None` for an empty store. When multiple values share the
4535    /// maximum byte length, the lexicographically largest is returned.
4536    pub fn value_with_max_bytes(&self) -> Result<Option<String>, AgentRuntimeError> {
4537        let inner = recover_lock(self.inner.lock(), "WorkingMemory::value_with_max_bytes");
4538        Ok(inner
4539            .map
4540            .values()
4541            .max_by(|a, b| a.len().cmp(&b.len()).then(a.cmp(b)))
4542            .cloned())
4543    }
4544
4545    /// Return all keys whose associated value has a byte length greater than
4546    /// `min_bytes`, sorted alphabetically.
4547    ///
4548    /// Returns an empty `Vec` when no key qualifies.
4549    pub fn values_longer_than(
4550        &self,
4551        min_bytes: usize,
4552    ) -> Result<Vec<String>, AgentRuntimeError> {
4553        let inner = recover_lock(self.inner.lock(), "WorkingMemory::values_longer_than");
4554        let mut keys: Vec<String> = inner
4555            .map
4556            .iter()
4557            .filter(|(_, v)| v.len() > min_bytes)
4558            .map(|(k, _)| k.clone())
4559            .collect();
4560        keys.sort_unstable();
4561        Ok(keys)
4562    }
4563
4564    /// Return `true` if any key in the store starts with `prefix`.
4565    pub fn has_key_with_prefix(&self, prefix: &str) -> Result<bool, AgentRuntimeError> {
4566        let inner = recover_lock(self.inner.lock(), "WorkingMemory::has_key_with_prefix");
4567        Ok(inner.map.keys().any(|k| k.starts_with(prefix)))
4568    }
4569
4570    /// Return the count of values that start with `prefix`.
4571    ///
4572    /// Returns `0` for an empty store or when no value matches.
4573    pub fn value_count_matching_prefix(
4574        &self,
4575        prefix: &str,
4576    ) -> Result<usize, AgentRuntimeError> {
4577        let inner =
4578            recover_lock(self.inner.lock(), "WorkingMemory::value_count_matching_prefix");
4579        Ok(inner.map.values().filter(|v| v.starts_with(prefix)).count())
4580    }
4581
4582    /// Return the sum of byte lengths of all values currently stored.
4583    ///
4584    /// Returns `0` for an empty store.
4585    pub fn value_bytes_total(&self) -> Result<usize, AgentRuntimeError> {
4586        let inner = recover_lock(self.inner.lock(), "WorkingMemory::value_bytes_total");
4587        Ok(inner.map.values().map(|v| v.len()).sum())
4588    }
4589
4590    /// Return all keys sorted alphabetically.
4591    ///
4592    /// Returns an empty `Vec` for an empty store.
4593    pub fn all_keys_sorted(&self) -> Result<Vec<String>, AgentRuntimeError> {
4594        let inner = recover_lock(self.inner.lock(), "WorkingMemory::all_keys_sorted");
4595        let mut keys: Vec<String> = inner.map.keys().cloned().collect();
4596        keys.sort_unstable();
4597        Ok(keys)
4598    }
4599
4600    /// Return all values in the store, sorted alphabetically.
4601    ///
4602    /// Useful for deterministic comparison in tests and diagnostics.
4603    /// Returns an empty `Vec` for an empty store.
4604    pub fn all_values_sorted(&self) -> Result<Vec<String>, AgentRuntimeError> {
4605        let inner = recover_lock(self.inner.lock(), "WorkingMemory::all_values_sorted");
4606        let mut values: Vec<String> = inner.map.values().cloned().collect();
4607        values.sort_unstable();
4608        Ok(values)
4609    }
4610
4611    /// Return the keys whose stored value begins with `prefix`.
4612    ///
4613    /// Returns an empty `Vec` when no value matches.
4614    pub fn keys_with_value_prefix(&self, prefix: &str) -> Result<Vec<String>, AgentRuntimeError> {
4615        let inner = recover_lock(self.inner.lock(), "WorkingMemory::keys_with_value_prefix");
4616        Ok(inner
4617            .map
4618            .iter()
4619            .filter(|(_, v)| v.starts_with(prefix))
4620            .map(|(k, _)| k.clone())
4621            .collect())
4622    }
4623
4624    /// Return the number of key-value pairs currently stored.
4625    pub fn key_count(&self) -> Result<usize, AgentRuntimeError> {
4626        let inner = recover_lock(self.inner.lock(), "WorkingMemory::key_count");
4627        Ok(inner.map.len())
4628    }
4629
4630    /// Return the value for `key`, or `default` if the key is not present.
4631    pub fn value_for_key_or_default<'a>(
4632        &self,
4633        key: &str,
4634        default: &'a str,
4635    ) -> Result<String, AgentRuntimeError> {
4636        let inner = recover_lock(self.inner.lock(), "WorkingMemory::value_for_key_or_default");
4637        Ok(inner
4638            .map
4639            .get(key)
4640            .map(|v| v.clone())
4641            .unwrap_or_else(|| default.to_string()))
4642    }
4643
4644    /// Return all `(key, value)` pairs whose key starts with `prefix`,
4645    /// sorted alphabetically by key.
4646    ///
4647    /// Returns an empty `Vec` for an empty store or when no key matches.
4648    pub fn entries_with_key_prefix(
4649        &self,
4650        prefix: &str,
4651    ) -> Result<Vec<(String, String)>, AgentRuntimeError> {
4652        let inner = recover_lock(self.inner.lock(), "WorkingMemory::entries_with_key_prefix");
4653        let mut entries: Vec<(String, String)> = inner
4654            .map
4655            .iter()
4656            .filter(|(k, _)| k.starts_with(prefix))
4657            .map(|(k, v)| (k.clone(), v.clone()))
4658            .collect();
4659        entries.sort_by(|a, b| a.0.cmp(&b.0));
4660        Ok(entries)
4661    }
4662
4663    /// Return `true` if any stored value is exactly equal to `val`.
4664    ///
4665    /// Returns `false` for an empty store.
4666    pub fn has_value_equal_to(&self, val: &str) -> Result<bool, AgentRuntimeError> {
4667        let inner = recover_lock(self.inner.lock(), "WorkingMemory::has_value_equal_to");
4668        Ok(inner.map.values().any(|v| v == val))
4669    }
4670
4671    /// Return `true` if the value stored under `key` contains `substr`.
4672    ///
4673    /// Returns `false` when `key` is not present.
4674    pub fn value_contains(&self, key: &str, substr: &str) -> Result<bool, AgentRuntimeError> {
4675        let inner = recover_lock(self.inner.lock(), "WorkingMemory::value_contains");
4676        Ok(inner.map.get(key).map_or(false, |v| v.contains(substr)))
4677    }
4678
4679    /// Return all keys that do NOT start with `prefix`, sorted alphabetically.
4680    ///
4681    /// Returns an empty `Vec` for an empty store or when all keys match.
4682    pub fn keys_without_prefix(&self, prefix: &str) -> Result<Vec<String>, AgentRuntimeError> {
4683        let inner = recover_lock(self.inner.lock(), "WorkingMemory::keys_without_prefix");
4684        let mut keys: Vec<String> = inner
4685            .map
4686            .keys()
4687            .filter(|k| !k.starts_with(prefix))
4688            .cloned()
4689            .collect();
4690        keys.sort_unstable();
4691        Ok(keys)
4692    }
4693
4694    /// Return the number of stored values that are exactly equal to `val`.
4695    pub fn count_values_equal_to(&self, val: &str) -> Result<usize, AgentRuntimeError> {
4696        let inner = recover_lock(self.inner.lock(), "WorkingMemory::count_values_equal_to");
4697        Ok(inner.map.values().filter(|v| v.as_str() == val).count())
4698    }
4699
4700    /// Return the number of keys whose byte length is strictly less than
4701    /// `max_bytes`.
4702    pub fn count_keys_below_bytes(&self, max_bytes: usize) -> Result<usize, AgentRuntimeError> {
4703        let inner = recover_lock(self.inner.lock(), "WorkingMemory::count_keys_below_bytes");
4704        Ok(inner.map.keys().filter(|k| k.len() < max_bytes).count())
4705    }
4706
4707    /// Return the number of Unicode scalar values (chars) in the value stored
4708    /// under `key`, or `0` if the key is not present.
4709    pub fn value_char_count(&self, key: &str) -> Result<usize, AgentRuntimeError> {
4710        let inner = recover_lock(self.inner.lock(), "WorkingMemory::value_char_count");
4711        Ok(inner.map.get(key).map_or(0, |v| v.chars().count()))
4712    }
4713
4714    /// Return all keys whose byte length is strictly greater than `min_bytes`.
4715    ///
4716    /// Returns an empty `Vec` when no keys satisfy the condition.
4717    pub fn keys_longer_than(&self, min_bytes: usize) -> Result<Vec<String>, AgentRuntimeError> {
4718        let inner = recover_lock(self.inner.lock(), "WorkingMemory::keys_longer_than");
4719        Ok(inner.map.keys().filter(|k| k.len() > min_bytes).cloned().collect())
4720    }
4721
4722    /// Return all keys whose byte length is strictly less than `max_bytes`.
4723    ///
4724    /// Returns an empty `Vec` when no keys satisfy the condition.
4725    pub fn keys_shorter_than(&self, max_bytes: usize) -> Result<Vec<String>, AgentRuntimeError> {
4726        let inner = recover_lock(self.inner.lock(), "WorkingMemory::keys_shorter_than");
4727        Ok(inner.map.keys().filter(|k| k.len() < max_bytes).cloned().collect())
4728    }
4729
4730    /// Return all stored values that start with `prefix`.
4731    pub fn values_with_prefix(&self, prefix: &str) -> Result<Vec<String>, AgentRuntimeError> {
4732        let inner = recover_lock(self.inner.lock(), "WorkingMemory::values_with_prefix");
4733        Ok(inner
4734            .map
4735            .values()
4736            .filter(|v| v.starts_with(prefix))
4737            .cloned()
4738            .collect())
4739    }
4740
4741    /// Return all values whose string representation ends with `suffix`.
4742    pub fn values_with_suffix(&self, suffix: &str) -> Result<Vec<String>, AgentRuntimeError> {
4743        let inner = recover_lock(self.inner.lock(), "WorkingMemory::values_with_suffix");
4744        Ok(inner
4745            .map
4746            .values()
4747            .filter(|v| v.ends_with(suffix))
4748            .cloned()
4749            .collect())
4750    }
4751
4752    /// Return `true` if the value stored under `key` starts with `prefix`.
4753    ///
4754    /// Returns `false` when `key` is not present.
4755    pub fn value_starts_with(&self, key: &str, prefix: &str) -> Result<bool, AgentRuntimeError> {
4756        let inner = recover_lock(self.inner.lock(), "WorkingMemory::value_starts_with");
4757        Ok(inner
4758            .map
4759            .get(key)
4760            .map_or(false, |v| v.starts_with(prefix)))
4761    }
4762
4763    /// Return a histogram of value byte lengths bucketed by `bucket_size`.
4764    ///
4765    /// The returned `Vec` contains `(bucket_start, count)` pairs where
4766    /// `bucket_start = (value_len / bucket_size) * bucket_size`.  Pairs are
4767    /// sorted by `bucket_start` in ascending order.  Returns an empty `Vec`
4768    /// for an empty store or when `bucket_size == 0`.
4769    pub fn value_length_histogram(
4770        &self,
4771        bucket_size: usize,
4772    ) -> Result<Vec<(usize, usize)>, AgentRuntimeError> {
4773        if bucket_size == 0 {
4774            return Ok(Vec::new());
4775        }
4776        let inner = recover_lock(self.inner.lock(), "WorkingMemory::value_length_histogram");
4777        let mut buckets: std::collections::HashMap<usize, usize> =
4778            std::collections::HashMap::new();
4779        for v in inner.map.values() {
4780            let bucket = (v.len() / bucket_size) * bucket_size;
4781            *buckets.entry(bucket).or_insert(0) += 1;
4782        }
4783        let mut result: Vec<(usize, usize)> = buckets.into_iter().collect();
4784        result.sort_unstable_by_key(|(k, _)| *k);
4785        Ok(result)
4786    }
4787
4788    /// Return the number of keys that contain `substr` as a substring.
4789    ///
4790    /// Useful for quickly counting "semantic" keys that share a common
4791    /// token (e.g. `"intent"`, `"context"`, `"goal"`).  Returns `0` for
4792    /// an empty store or when no key matches.
4793    pub fn semantic_key_count(&self, substr: &str) -> Result<usize, AgentRuntimeError> {
4794        let inner = recover_lock(self.inner.lock(), "WorkingMemory::semantic_key_count");
4795        Ok(inner.map.keys().filter(|k| k.contains(substr)).count())
4796    }
4797
4798    /// Return the byte length of the longest stored value, or `0` when empty.
4799    ///
4800    /// Helpful for detecting unusually large values that might indicate a
4801    /// working-memory bloat.  Returns `0` for an empty store.
4802    pub fn longest_value_bytes(&self) -> Result<usize, AgentRuntimeError> {
4803        let inner = recover_lock(self.inner.lock(), "WorkingMemory::longest_value_bytes");
4804        Ok(inner.map.values().map(|v| v.len()).max().unwrap_or(0))
4805    }
4806
4807    /// Return the shortest key length, or `0` if the store is empty.
4808    pub fn min_key_length(&self) -> Result<usize, AgentRuntimeError> {
4809        let inner = recover_lock(self.inner.lock(), "WorkingMemory::min_key_length");
4810        Ok(inner.map.keys().map(|k| k.len()).min().unwrap_or(0))
4811    }
4812
4813    /// Return the number of keys that start with the given prefix.
4814    pub fn count_matching_prefix(&self, prefix: &str) -> Result<usize, AgentRuntimeError> {
4815        let inner = recover_lock(self.inner.lock(), "WorkingMemory::count_matching_prefix");
4816        Ok(inner.map.keys().filter(|k| k.starts_with(prefix)).count())
4817    }
4818
4819    /// Return a list of `(key, value_byte_length)` pairs for all entries.
4820    /// Return the number of key-value entries currently stored.
4821    pub fn entry_count(&self) -> Result<usize, AgentRuntimeError> {
4822        let inner = recover_lock(self.inner.lock(), "WorkingMemory::entry_count");
4823        Ok(inner.map.len())
4824    }
4825
4826    /// Return a list of `(key, value_byte_length)` pairs for all entries.
4827    pub fn value_lengths(&self) -> Result<Vec<(String, usize)>, AgentRuntimeError> {
4828        let inner = recover_lock(self.inner.lock(), "WorkingMemory::value_lengths");
4829        Ok(inner.map.iter().map(|(k, v)| (k.clone(), v.len())).collect())
4830    }
4831
4832    /// Return the keys of all entries whose value byte length is strictly
4833    /// greater than `threshold`.
4834    pub fn keys_with_value_longer_than(&self, threshold: usize) -> Result<Vec<String>, AgentRuntimeError> {
4835        let inner = recover_lock(self.inner.lock(), "WorkingMemory::keys_with_value_longer_than");
4836        Ok(inner
4837            .map
4838            .iter()
4839            .filter(|(_, v)| v.len() > threshold)
4840            .map(|(k, _)| k.clone())
4841            .collect())
4842    }
4843
4844    /// Return the key whose associated value has the most bytes, or `None` if empty.
4845    pub fn longest_value_key(&self) -> Result<Option<String>, AgentRuntimeError> {
4846        let inner = recover_lock(self.inner.lock(), "WorkingMemory::longest_value_key");
4847        Ok(inner
4848            .map
4849            .iter()
4850            .max_by_key(|(_, v)| v.len())
4851            .map(|(k, _)| k.clone()))
4852    }
4853
4854    /// Remove all entries for which `predicate(key, value)` returns `false`.
4855    ///
4856    /// Preserves insertion order of the surviving entries.
4857    /// Returns the number of entries removed.
4858    pub fn retain<F>(&self, mut predicate: F) -> Result<usize, AgentRuntimeError>
4859    where
4860        F: FnMut(&str, &str) -> bool,
4861    {
4862        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::retain");
4863        let before = inner.map.len();
4864        inner.map.retain(|k, v| predicate(k.as_str(), v.as_str()));
4865        let surviving: std::collections::HashSet<String> =
4866            inner.map.keys().cloned().collect();
4867        inner.order.retain(|k| surviving.contains(k));
4868        Ok(before - inner.map.len())
4869    }
4870
4871    /// Copy all entries from `other` into `self`.
4872    ///
4873    /// Entries from `other` are inserted in its insertion order.  Capacity
4874    /// limits and eviction apply as if each entry were inserted individually
4875    /// via [`set`].
4876    ///
4877    /// [`set`]: WorkingMemory::set
4878    pub fn merge_from(&self, other: &WorkingMemory) -> Result<usize, AgentRuntimeError> {
4879        let pairs: Vec<(String, String)> = {
4880            let inner = recover_lock(other.inner.lock(), "WorkingMemory::merge_from(read)");
4881            inner.order.iter().filter_map(|k| {
4882                inner.map.get(k).map(|v| (k.clone(), v.clone()))
4883            }).collect()
4884        };
4885        let count = pairs.len();
4886        self.set_many(pairs)?;
4887        Ok(count)
4888    }
4889
4890    /// Return the number of entries for which `predicate(key, value)` returns `true`.
4891    pub fn entry_count_satisfying<F>(&self, mut predicate: F) -> Result<usize, AgentRuntimeError>
4892    where
4893        F: FnMut(&str, &str) -> bool,
4894    {
4895        let inner = recover_lock(self.inner.lock(), "WorkingMemory::entry_count_satisfying");
4896        Ok(inner.map.iter().filter(|(k, v)| predicate(k.as_str(), v.as_str())).count())
4897    }
4898
4899    /// Return all keys in insertion order.
4900    pub fn get_all_keys(&self) -> Result<Vec<String>, AgentRuntimeError> {
4901        let inner = recover_lock(self.inner.lock(), "WorkingMemory::get_all_keys");
4902        Ok(inner.order.iter().cloned().collect())
4903    }
4904
4905    /// Atomically replace **all** entries with `map`.
4906    ///
4907    /// The new entries are stored in iteration order of `map`.  Capacity
4908    /// limits apply: if `map.len() > capacity`, only the last `capacity`
4909    /// entries (in iteration order) are retained.
4910    pub fn replace_all(
4911        &self,
4912        map: std::collections::HashMap<String, String>,
4913    ) -> Result<(), AgentRuntimeError> {
4914        let capacity = self.capacity;
4915        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::replace_all");
4916        inner.map.clear();
4917        inner.order.clear();
4918        for (k, v) in map {
4919            if inner.map.len() >= capacity {
4920                if let Some(oldest) = inner.order.pop_front() {
4921                    inner.map.remove(&oldest);
4922                }
4923            }
4924            inner.order.push_back(k.clone());
4925            inner.map.insert(k, v);
4926        }
4927        Ok(())
4928    }
4929
4930    /// Return all values whose content contains `substring` (case-sensitive),
4931    /// in insertion order.
4932    ///
4933    /// Returns an empty `Vec` when no values match or the store is empty.
4934    pub fn values_containing(&self, substring: &str) -> Result<Vec<String>, AgentRuntimeError> {
4935        let inner = recover_lock(self.inner.lock(), "WorkingMemory::values_containing");
4936        let result = inner
4937            .order
4938            .iter()
4939            .filter_map(|k| {
4940                inner.map.get(k).filter(|v| v.contains(substring)).cloned()
4941            })
4942            .collect();
4943        Ok(result)
4944    }
4945
4946    /// Return all keys in ascending sorted (lexicographic) order.
4947    ///
4948    /// Unlike [`get_all_keys`] which returns keys in insertion order, this
4949    /// method always returns them in a deterministic alphabetical order.
4950    ///
4951    /// [`get_all_keys`]: WorkingMemory::get_all_keys
4952    pub fn keys_sorted(&self) -> Result<Vec<String>, AgentRuntimeError> {
4953        let inner = recover_lock(self.inner.lock(), "WorkingMemory::keys_sorted");
4954        let mut keys: Vec<String> = inner.map.keys().cloned().collect();
4955        keys.sort_unstable();
4956        Ok(keys)
4957    }
4958
4959    /// Return the value associated with the longest key, or `None` if the
4960    /// store is empty.
4961    ///
4962    /// When multiple keys share the maximum byte length, the one that sorts
4963    /// first lexicographically is chosen for deterministic output.
4964    pub fn value_for_longest_key(&self) -> Result<Option<String>, AgentRuntimeError> {
4965        let inner = recover_lock(self.inner.lock(), "WorkingMemory::value_for_longest_key");
4966        let best_key = inner
4967            .map
4968            .keys()
4969            .max_by(|a, b| a.len().cmp(&b.len()).then_with(|| b.cmp(a)));
4970        Ok(best_key.and_then(|k| inner.map.get(k).cloned()))
4971    }
4972
4973    /// Return `true` if any entry in this store has a value equal to `value`.
4974    ///
4975    /// Performs a linear scan over all stored values.  For exact-match
4976    /// semantics use [`get`] instead.
4977    ///
4978    /// [`get`]: WorkingMemory::get
4979    pub fn contains_value(&self, value: &str) -> Result<bool, AgentRuntimeError> {
4980        let inner = recover_lock(self.inner.lock(), "WorkingMemory::contains_value");
4981        Ok(inner.map.values().any(|v| v == value))
4982    }
4983
4984    /// Return the key with the fewest bytes, or `None` if the store is empty.
4985    ///
4986    /// When multiple keys share the minimum byte length, the one that sorts
4987    /// first lexicographically is returned for deterministic output.
4988    pub fn shortest_key(&self) -> Result<Option<String>, AgentRuntimeError> {
4989        let inner = recover_lock(self.inner.lock(), "WorkingMemory::shortest_key");
4990        Ok(inner
4991            .map
4992            .keys()
4993            .min_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b)))
4994            .cloned())
4995    }
4996
4997    /// Return a `Vec` of `(key_byte_len, value_byte_len)` pairs for every entry.
4998    ///
4999    /// The order of the returned pairs is unspecified (hash-map iteration order).
5000    /// Returns an empty `Vec` for an empty store.
5001    pub fn entry_byte_pairs(&self) -> Result<Vec<(usize, usize)>, AgentRuntimeError> {
5002        let inner = recover_lock(self.inner.lock(), "WorkingMemory::entry_byte_pairs");
5003        Ok(inner.map.iter().map(|(k, v)| (k.len(), v.len())).collect())
5004    }
5005
5006    /// Return the total byte usage of the store — the sum of all key and value
5007    /// byte lengths.
5008    ///
5009    /// Returns `0` for an empty store.
5010    pub fn total_bytes(&self) -> Result<usize, AgentRuntimeError> {
5011        let inner = recover_lock(self.inner.lock(), "WorkingMemory::total_bytes");
5012        Ok(inner.map.iter().map(|(k, v)| k.len() + v.len()).sum())
5013    }
5014
5015    /// Return the key whose value has the greatest byte length, or `None` if
5016    /// the store is empty.
5017    ///
5018    /// When multiple values share the maximum byte length, the key that sorts
5019    /// first lexicographically is returned for deterministic output.
5020    pub fn key_with_longest_value(&self) -> Result<Option<String>, AgentRuntimeError> {
5021        let inner = recover_lock(self.inner.lock(), "WorkingMemory::key_with_longest_value");
5022        Ok(inner
5023            .map
5024            .iter()
5025            .max_by(|(ka, va), (kb, vb)| va.len().cmp(&vb.len()).then_with(|| kb.cmp(ka)))
5026            .map(|(k, _)| k.clone()))
5027    }
5028
5029    /// Return the number of entries whose value byte length is strictly less than `max_bytes`.
5030    ///
5031    /// Returns `0` for an empty store.
5032    pub fn count_below_value_length(&self, max_bytes: usize) -> Result<usize, AgentRuntimeError> {
5033        let inner = recover_lock(self.inner.lock(), "WorkingMemory::count_below_value_length");
5034        Ok(inner.map.values().filter(|v| v.len() < max_bytes).count())
5035    }
5036
5037    /// Return the value associated with the shortest key, or `None` if the
5038    /// store is empty.
5039    ///
5040    /// When multiple keys share the minimum byte length, the one that sorts
5041    /// first lexicographically is selected for deterministic output.
5042    pub fn value_for_shortest_key(&self) -> Result<Option<String>, AgentRuntimeError> {
5043        let inner = recover_lock(self.inner.lock(), "WorkingMemory::value_for_shortest_key");
5044        let best_key = inner
5045            .map
5046            .keys()
5047            .min_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b)));
5048        Ok(best_key.and_then(|k| inner.map.get(k).cloned()))
5049    }
5050
5051    /// Return all `(key, value)` pairs whose value byte length is strictly
5052    /// greater than `min_value_bytes`.
5053    ///
5054    /// Returns an empty `Vec` for an empty store or when no values exceed the
5055    /// threshold.
5056    pub fn key_value_pairs_above_length(
5057        &self,
5058        min_value_bytes: usize,
5059    ) -> Result<Vec<(String, String)>, AgentRuntimeError> {
5060        let inner = recover_lock(self.inner.lock(), "WorkingMemory::key_value_pairs_above_length");
5061        let pairs: Vec<(String, String)> = inner
5062            .map
5063            .iter()
5064            .filter(|(_, v)| v.len() > min_value_bytes)
5065            .map(|(k, v)| (k.clone(), v.clone()))
5066            .collect();
5067        Ok(pairs)
5068    }
5069
5070    /// Return the value for `key` if it exists, otherwise insert `default` and
5071    /// return it.
5072    ///
5073    /// The returned `String` is always the value associated with `key` after
5074    /// this call completes.  If the key was absent and the store has a capacity
5075    /// limit, the oldest entry may be evicted to make room for the new entry.
5076    pub fn get_or_insert(
5077        &self,
5078        key: impl Into<String>,
5079        default: impl Into<String>,
5080    ) -> Result<String, AgentRuntimeError> {
5081        let key = key.into();
5082        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::get_or_insert");
5083        if let Some(v) = inner.map.get(&key) {
5084            return Ok(v.clone());
5085        }
5086        let value = default.into();
5087        // Evict oldest entry if at capacity.
5088        let cap = self.capacity;
5089        if inner.map.len() >= cap && cap > 0 {
5090            if let Some(oldest) = inner.order.pop_front() {
5091                inner.map.remove(&oldest);
5092            }
5093        }
5094        inner.map.insert(key.clone(), value.clone());
5095        inner.order.push_back(key);
5096        Ok(value)
5097    }
5098
5099    /// Parse the value for `key` as an `i64`, add `step`, and store the result.
5100    ///
5101    /// If `key` does not exist it is treated as `0` before adding `step`.
5102    /// Returns the new value after the increment.
5103    ///
5104    /// # Errors
5105    /// Returns `AgentRuntimeError::Memory` if the existing value cannot be
5106    /// parsed as `i64`.
5107    pub fn increment(&self, key: impl Into<String>, step: i64) -> Result<i64, AgentRuntimeError> {
5108        let key = key.into();
5109        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::increment");
5110        let current: i64 = if let Some(v) = inner.map.get(&key) {
5111            v.parse::<i64>().map_err(|_| {
5112                AgentRuntimeError::Memory(format!(
5113                    "WorkingMemory::increment: value for key '{key}' is not a valid integer"
5114                ))
5115            })?
5116        } else {
5117            0
5118        };
5119        let new_val = current.saturating_add(step);
5120        let new_str = new_val.to_string();
5121        if !inner.map.contains_key(&key) {
5122            // Evict oldest if at capacity.
5123            let cap = self.capacity;
5124            if inner.map.len() >= cap && cap > 0 {
5125                if let Some(oldest) = inner.order.pop_front() {
5126                    inner.map.remove(&oldest);
5127                }
5128            }
5129            inner.order.push_back(key.clone());
5130        }
5131        inner.map.insert(key, new_str);
5132        Ok(new_val)
5133    }
5134
5135    /// Swap the values of `key_a` and `key_b`.
5136    ///
5137    /// Returns `Ok(())` when both keys exist and the swap succeeds.
5138    /// Returns `Err` when either key is absent — the store is left unchanged.
5139    pub fn swap(&self, key_a: &str, key_b: &str) -> Result<(), AgentRuntimeError> {
5140        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::swap");
5141        let a = inner.map.get(key_a).cloned().ok_or_else(|| {
5142            AgentRuntimeError::Memory(format!("WorkingMemory::swap: key '{key_a}' not found"))
5143        })?;
5144        let b = inner.map.get(key_b).cloned().ok_or_else(|| {
5145            AgentRuntimeError::Memory(format!("WorkingMemory::swap: key '{key_b}' not found"))
5146        })?;
5147        inner.map.insert(key_a.to_owned(), b);
5148        inner.map.insert(key_b.to_owned(), a);
5149        Ok(())
5150    }
5151
5152    /// Return all values in the store that are non-empty strings.
5153    ///
5154    /// The returned `Vec` is in insertion order.  Returns an empty `Vec` for
5155    /// an empty store or when all values are empty strings.
5156    pub fn non_empty_values(&self) -> Result<Vec<String>, AgentRuntimeError> {
5157        let inner = recover_lock(self.inner.lock(), "WorkingMemory::non_empty_values");
5158        let values: Vec<String> = inner
5159            .order
5160            .iter()
5161            .filter_map(|k| inner.map.get(k))
5162            .filter(|v| !v.is_empty())
5163            .cloned()
5164            .collect();
5165        Ok(values)
5166    }
5167
5168    /// Return all values sorted alphabetically.
5169    ///
5170    /// The sort is lexicographic (byte order).  Duplicate values are preserved.
5171    pub fn values_sorted(&self) -> Result<Vec<String>, AgentRuntimeError> {
5172        let inner = recover_lock(self.inner.lock(), "WorkingMemory::values_sorted");
5173        let mut values: Vec<String> = inner.map.values().cloned().collect();
5174        values.sort_unstable();
5175        Ok(values)
5176    }
5177
5178    /// Return the number of keys whose byte length is strictly greater than
5179    /// `min_bytes`.
5180    ///
5181    /// Returns `0` for an empty store or when no key qualifies.
5182    pub fn count_keys_above_bytes(&self, min_bytes: usize) -> Result<usize, AgentRuntimeError> {
5183        let inner = recover_lock(self.inner.lock(), "WorkingMemory::count_keys_above_bytes");
5184        Ok(inner.map.keys().filter(|k| k.len() > min_bytes).count())
5185    }
5186
5187    /// Return `true` if any two distinct keys share the same value.
5188    ///
5189    /// Returns `false` for an empty store or a store with only one entry.
5190    pub fn has_duplicate_values(&self) -> Result<bool, AgentRuntimeError> {
5191        let inner = recover_lock(self.inner.lock(), "WorkingMemory::has_duplicate_values");
5192        let mut seen = std::collections::HashSet::new();
5193        for v in inner.map.values() {
5194            if !seen.insert(v.as_str()) {
5195                return Ok(true);
5196            }
5197        }
5198        Ok(false)
5199    }
5200
5201    /// Return the value with the fewest bytes, or `None` for an empty store.
5202    ///
5203    /// When multiple values share the minimum byte length the lexicographically
5204    /// smallest one is returned for deterministic output.
5205    pub fn shortest_value(&self) -> Result<Option<String>, AgentRuntimeError> {
5206        let inner = recover_lock(self.inner.lock(), "WorkingMemory::shortest_value");
5207        Ok(inner
5208            .map
5209            .values()
5210            .min_by(|a, b| a.len().cmp(&b.len()).then(a.cmp(b)))
5211            .cloned())
5212    }
5213
5214    /// Return all keys whose stored value exactly equals `value`, sorted
5215    /// alphabetically.
5216    ///
5217    /// Returns an empty `Vec` when no key has that value or the store is empty.
5218    pub fn keys_with_value_equal_to(&self, value: &str) -> Result<Vec<String>, AgentRuntimeError> {
5219        let inner =
5220            recover_lock(self.inner.lock(), "WorkingMemory::keys_with_value_equal_to");
5221        let mut keys: Vec<String> = inner
5222            .map
5223            .iter()
5224            .filter(|(_, v)| v.as_str() == value)
5225            .map(|(k, _)| k.clone())
5226            .collect();
5227        keys.sort_unstable();
5228        Ok(keys)
5229    }
5230
5231    /// Return `true` if every value currently stored in working memory is
5232    /// non-empty.
5233    ///
5234    /// Returns `true` when the store is empty (vacuously true).  Callers that
5235    /// treat empty values as invalid can use this to validate the store
5236    /// contents in a single call.
5237    pub fn all_values_non_empty(&self) -> Result<bool, AgentRuntimeError> {
5238        let inner =
5239            recover_lock(self.inner.lock(), "WorkingMemory::all_values_non_empty");
5240        Ok(inner.map.values().all(|v| !v.is_empty()))
5241    }
5242
5243    /// Return the average byte length of all stored values.
5244    ///
5245    /// Returns `0.0` when the store is empty to avoid division-by-zero.
5246    pub fn average_value_length(&self) -> Result<f64, AgentRuntimeError> {
5247        let inner =
5248            recover_lock(self.inner.lock(), "WorkingMemory::average_value_length");
5249        let count = inner.map.len();
5250        if count == 0 {
5251            return Ok(0.0);
5252        }
5253        let total: usize = inner.map.values().map(|v| v.len()).sum();
5254        Ok(total as f64 / count as f64)
5255    }
5256
5257}
5258
5259// ── Tests ─────────────────────────────────────────────────────────────────────
5260
5261#[cfg(test)]
5262mod tests {
5263    use super::*;
5264
5265    // ── AgentId / MemoryId ────────────────────────────────────────────────────
5266
5267    #[test]
5268    fn test_agent_id_new_stores_string() {
5269        let id = AgentId::new("agent-1");
5270        assert_eq!(id.0, "agent-1");
5271    }
5272
5273    #[test]
5274    fn test_agent_id_random_is_unique() {
5275        let a = AgentId::random();
5276        let b = AgentId::random();
5277        assert_ne!(a, b);
5278    }
5279
5280    #[test]
5281    fn test_memory_id_new_stores_string() {
5282        let id = MemoryId::new("mem-1");
5283        assert_eq!(id.0, "mem-1");
5284    }
5285
5286    #[test]
5287    fn test_memory_id_random_is_unique() {
5288        let a = MemoryId::random();
5289        let b = MemoryId::random();
5290        assert_ne!(a, b);
5291    }
5292
5293    // ── MemoryItem ────────────────────────────────────────────────────────────
5294
5295    #[test]
5296    fn test_memory_item_new_clamps_importance_above_one() {
5297        let item = MemoryItem::new(AgentId::new("a"), "test", 1.5, vec![]);
5298        assert_eq!(item.importance, 1.0);
5299    }
5300
5301    #[test]
5302    fn test_memory_item_new_clamps_importance_below_zero() {
5303        let item = MemoryItem::new(AgentId::new("a"), "test", -0.5, vec![]);
5304        assert_eq!(item.importance, 0.0);
5305    }
5306
5307    #[test]
5308    fn test_memory_item_new_preserves_valid_importance() {
5309        let item = MemoryItem::new(AgentId::new("a"), "test", 0.7, vec![]);
5310        assert!((item.importance - 0.7).abs() < 1e-6);
5311    }
5312
5313    // ── DecayPolicy ───────────────────────────────────────────────────────────
5314
5315    #[test]
5316    fn test_decay_policy_rejects_zero_half_life() {
5317        assert!(DecayPolicy::exponential(0.0).is_err());
5318    }
5319
5320    #[test]
5321    fn test_decay_policy_rejects_negative_half_life() {
5322        assert!(DecayPolicy::exponential(-1.0).is_err());
5323    }
5324
5325    #[test]
5326    fn test_decay_policy_no_decay_at_age_zero() {
5327        let p = DecayPolicy::exponential(24.0).unwrap();
5328        let decayed = p.apply(1.0, 0.0);
5329        assert!((decayed - 1.0).abs() < 1e-5);
5330    }
5331
5332    #[test]
5333    fn test_decay_policy_half_importance_at_half_life() {
5334        let p = DecayPolicy::exponential(24.0).unwrap();
5335        let decayed = p.apply(1.0, 24.0);
5336        assert!((decayed - 0.5).abs() < 1e-5);
5337    }
5338
5339    #[test]
5340    fn test_decay_policy_quarter_importance_at_two_half_lives() {
5341        let p = DecayPolicy::exponential(24.0).unwrap();
5342        let decayed = p.apply(1.0, 48.0);
5343        assert!((decayed - 0.25).abs() < 1e-5);
5344    }
5345
5346    #[test]
5347    fn test_decay_policy_result_is_clamped_to_zero_one() {
5348        let p = DecayPolicy::exponential(1.0).unwrap();
5349        let decayed = p.apply(0.0, 1000.0);
5350        assert!(decayed >= 0.0 && decayed <= 1.0);
5351    }
5352
5353    // ── EpisodicStore ─────────────────────────────────────────────────────────
5354
5355    #[test]
5356    fn test_episodic_store_add_episode_returns_id() {
5357        let store = EpisodicStore::new();
5358        let id = store.add_episode(AgentId::new("a"), "event", 0.8).unwrap();
5359        assert!(!id.0.is_empty());
5360    }
5361
5362    #[test]
5363    fn test_episodic_store_recall_returns_stored_item() {
5364        let store = EpisodicStore::new();
5365        let agent = AgentId::new("agent-1");
5366        store
5367            .add_episode(agent.clone(), "hello world", 0.9)
5368            .unwrap();
5369        let items = store.recall(&agent, 10).unwrap();
5370        assert_eq!(items.len(), 1);
5371        assert_eq!(items[0].content, "hello world");
5372    }
5373
5374    #[test]
5375    fn test_episodic_store_recall_filters_by_agent() {
5376        let store = EpisodicStore::new();
5377        let a = AgentId::new("agent-a");
5378        let b = AgentId::new("agent-b");
5379        store.add_episode(a.clone(), "for a", 0.5).unwrap();
5380        store.add_episode(b.clone(), "for b", 0.5).unwrap();
5381        let items = store.recall(&a, 10).unwrap();
5382        assert_eq!(items.len(), 1);
5383        assert_eq!(items[0].content, "for a");
5384    }
5385
5386    #[test]
5387    fn test_episodic_store_recall_sorted_by_descending_importance() {
5388        let store = EpisodicStore::new();
5389        let agent = AgentId::new("agent-1");
5390        store.add_episode(agent.clone(), "low", 0.1).unwrap();
5391        store.add_episode(agent.clone(), "high", 0.9).unwrap();
5392        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
5393        let items = store.recall(&agent, 10).unwrap();
5394        assert_eq!(items[0].content, "high");
5395        assert_eq!(items[1].content, "mid");
5396        assert_eq!(items[2].content, "low");
5397    }
5398
5399    #[test]
5400    fn test_episodic_store_recall_respects_limit() {
5401        let store = EpisodicStore::new();
5402        let agent = AgentId::new("agent-1");
5403        for i in 0..5 {
5404            store
5405                .add_episode(agent.clone(), format!("item {i}"), 0.5)
5406                .unwrap();
5407        }
5408        let items = store.recall(&agent, 3).unwrap();
5409        assert_eq!(items.len(), 3);
5410    }
5411
5412    #[test]
5413    fn test_episodic_store_len_tracks_insertions() {
5414        let store = EpisodicStore::new();
5415        let agent = AgentId::new("a");
5416        store.add_episode(agent.clone(), "a", 0.5).unwrap();
5417        store.add_episode(agent.clone(), "b", 0.5).unwrap();
5418        assert_eq!(store.len().unwrap(), 2);
5419    }
5420
5421    #[test]
5422    fn test_episodic_store_is_empty_initially() {
5423        let store = EpisodicStore::new();
5424        assert!(store.is_empty().unwrap());
5425    }
5426
5427    #[test]
5428    fn test_episodic_store_with_decay_reduces_importance() {
5429        let policy = DecayPolicy::exponential(0.001).unwrap(); // very fast decay
5430        let store = EpisodicStore::with_decay(policy);
5431        let agent = AgentId::new("a");
5432
5433        // Insert an old item by manipulating via add_episode_at.
5434        let old_ts = Utc::now() - chrono::Duration::hours(1);
5435        store
5436            .add_episode_at(agent.clone(), "old event", 1.0, old_ts)
5437            .unwrap();
5438
5439        let items = store.recall(&agent, 10).unwrap();
5440        // With half_life=0.001h and age=1h, importance should be near 0
5441        assert_eq!(items.len(), 1);
5442        assert!(
5443            items[0].importance < 0.01,
5444            "expected near-zero importance, got {}",
5445            items[0].importance
5446        );
5447    }
5448
5449    // ── max_age eviction ──────────────────────────────────────────────────────
5450
5451    #[test]
5452    fn test_max_age_rejects_zero() {
5453        assert!(EpisodicStore::with_max_age(0.0).is_err());
5454    }
5455
5456    #[test]
5457    fn test_max_age_rejects_negative() {
5458        assert!(EpisodicStore::with_max_age(-1.0).is_err());
5459    }
5460
5461    #[test]
5462    fn test_max_age_evicts_old_items_on_recall() {
5463        // max_age = 0.001 hours (~3.6 seconds) — items 1 hour old should be evicted
5464        let store = EpisodicStore::with_max_age(0.001).unwrap();
5465        let agent = AgentId::new("a");
5466
5467        let old_ts = Utc::now() - chrono::Duration::hours(1);
5468        store
5469            .add_episode_at(agent.clone(), "old", 0.9, old_ts)
5470            .unwrap();
5471        store.add_episode(agent.clone(), "new", 0.5).unwrap();
5472
5473        let items = store.recall(&agent, 10).unwrap();
5474        assert_eq!(items.len(), 1, "old item should be evicted by max_age");
5475        assert_eq!(items[0].content, "new");
5476    }
5477
5478    #[test]
5479    fn test_max_age_evicts_old_items_on_add() {
5480        let store = EpisodicStore::with_max_age(0.001).unwrap();
5481        let agent = AgentId::new("a");
5482
5483        let old_ts = Utc::now() - chrono::Duration::hours(1);
5484        store
5485            .add_episode_at(agent.clone(), "old", 0.9, old_ts)
5486            .unwrap();
5487        // Adding a new item triggers stale purge.
5488        store.add_episode(agent.clone(), "new", 0.5).unwrap();
5489
5490        assert_eq!(store.len().unwrap(), 1);
5491    }
5492
5493    // ── RecallPolicy / per-agent capacity tests ───────────────────────────────
5494
5495    #[test]
5496    fn test_recall_increments_recall_count() {
5497        let store = EpisodicStore::new();
5498        let agent = AgentId::new("agent-rc");
5499        store.add_episode(agent.clone(), "memory", 0.5).unwrap();
5500
5501        // First recall — count becomes 1
5502        let items = store.recall(&agent, 10).unwrap();
5503        assert_eq!(items[0].recall_count, 1);
5504
5505        // Second recall — count becomes 2
5506        let items = store.recall(&agent, 10).unwrap();
5507        assert_eq!(items[0].recall_count, 2);
5508    }
5509
5510    #[test]
5511    fn test_hybrid_recall_policy_prefers_recently_used() {
5512        let store = EpisodicStore::with_recall_policy(RecallPolicy::Hybrid {
5513            recency_weight: 0.1,
5514            frequency_weight: 2.0,
5515        });
5516        let agent = AgentId::new("agent-hybrid");
5517
5518        let old_ts = Utc::now() - chrono::Duration::hours(48);
5519        store
5520            .add_episode_at(agent.clone(), "old_frequent", 0.5, old_ts)
5521            .unwrap();
5522        store.add_episode(agent.clone(), "new_never", 0.5).unwrap();
5523
5524        // Simulate many prior recalls of "old_frequent".
5525        store.bump_recall_count_by_content("old_frequent", 100);
5526
5527        let items = store.recall(&agent, 10).unwrap();
5528        assert_eq!(items.len(), 2);
5529        assert_eq!(
5530            items[0].content, "old_frequent",
5531            "hybrid policy should rank the frequently-recalled item first"
5532        );
5533    }
5534
5535    #[test]
5536    fn test_per_agent_capacity_evicts_lowest_importance() {
5537        let store = EpisodicStore::with_per_agent_capacity(2);
5538        let agent = AgentId::new("agent-cap");
5539
5540        // Add two items; capacity is full.
5541        store.add_episode(agent.clone(), "low", 0.1).unwrap();
5542        store.add_episode(agent.clone(), "high", 0.9).unwrap();
5543        // Adding "new" triggers eviction of the EXISTING lowest-importance item
5544        // ("low", 0.1) — the newly added item is never the one evicted.
5545        store.add_episode(agent.clone(), "new", 0.5).unwrap();
5546
5547        assert_eq!(
5548            store.len().unwrap(),
5549            2,
5550            "store should hold exactly 2 items after eviction"
5551        );
5552
5553        let items = store.recall(&agent, 10).unwrap();
5554        let contents: Vec<&str> = items.iter().map(|i| i.content.as_str()).collect();
5555        assert!(
5556            !contents.contains(&"low"),
5557            "the pre-existing lowest-importance item should have been evicted; remaining: {:?}",
5558            contents
5559        );
5560        assert!(
5561            contents.contains(&"new"),
5562            "the newly added item must never be evicted; remaining: {:?}",
5563            contents
5564        );
5565    }
5566
5567    // ── O(1) agent isolation ──────────────────────────────────────────────────
5568
5569    #[test]
5570    fn test_many_agents_do_not_see_each_others_memories() {
5571        let store = EpisodicStore::new();
5572        let n_agents = 20usize;
5573        for i in 0..n_agents {
5574            let agent = AgentId::new(format!("agent-{i}"));
5575            for j in 0..5 {
5576                store
5577                    .add_episode(agent.clone(), format!("item-{i}-{j}"), 0.5)
5578                    .unwrap();
5579            }
5580        }
5581        // Each agent should only see its own 5 items.
5582        for i in 0..n_agents {
5583            let agent = AgentId::new(format!("agent-{i}"));
5584            let items = store.recall(&agent, 100).unwrap();
5585            assert_eq!(
5586                items.len(),
5587                5,
5588                "agent {i} should see exactly 5 items, got {}",
5589                items.len()
5590            );
5591            for item in &items {
5592                assert!(
5593                    item.content.starts_with(&format!("item-{i}-")),
5594                    "agent {i} saw foreign item: {}",
5595                    item.content
5596                );
5597            }
5598        }
5599    }
5600
5601    // ── agent_memory_count / list_agents / purge_agent_memories ──────────────
5602
5603    #[test]
5604    fn test_agent_memory_count_returns_zero_for_unknown_agent() {
5605        let store = EpisodicStore::new();
5606        let count = store.agent_memory_count(&AgentId::new("ghost")).unwrap();
5607        assert_eq!(count, 0);
5608    }
5609
5610    #[test]
5611    fn test_agent_memory_count_tracks_insertions() {
5612        let store = EpisodicStore::new();
5613        let agent = AgentId::new("a");
5614        store.add_episode(agent.clone(), "e1", 0.5).unwrap();
5615        store.add_episode(agent.clone(), "e2", 0.5).unwrap();
5616        assert_eq!(store.agent_memory_count(&agent).unwrap(), 2);
5617    }
5618
5619    #[test]
5620    fn test_list_agents_returns_all_known_agents() {
5621        let store = EpisodicStore::new();
5622        let a = AgentId::new("agent-a");
5623        let b = AgentId::new("agent-b");
5624        store.add_episode(a.clone(), "x", 0.5).unwrap();
5625        store.add_episode(b.clone(), "y", 0.5).unwrap();
5626        let agents = store.list_agents().unwrap();
5627        assert_eq!(agents.len(), 2);
5628        assert!(agents.contains(&a));
5629        assert!(agents.contains(&b));
5630    }
5631
5632    #[test]
5633    fn test_list_agents_empty_when_no_episodes() {
5634        let store = EpisodicStore::new();
5635        let agents = store.list_agents().unwrap();
5636        assert!(agents.is_empty());
5637    }
5638
5639    #[test]
5640    fn test_purge_agent_memories_removes_all_for_agent() {
5641        let store = EpisodicStore::new();
5642        let a = AgentId::new("a");
5643        let b = AgentId::new("b");
5644        store.add_episode(a.clone(), "ep1", 0.5).unwrap();
5645        store.add_episode(a.clone(), "ep2", 0.5).unwrap();
5646        store.add_episode(b.clone(), "ep-b", 0.5).unwrap();
5647
5648        let removed = store.purge_agent_memories(&a).unwrap();
5649        assert_eq!(removed, 2);
5650        assert_eq!(store.agent_memory_count(&a).unwrap(), 0);
5651        assert_eq!(store.agent_memory_count(&b).unwrap(), 1);
5652        assert_eq!(store.len().unwrap(), 1);
5653    }
5654
5655    #[test]
5656    fn test_purge_agent_memories_returns_zero_for_unknown_agent() {
5657        let store = EpisodicStore::new();
5658        let removed = store.purge_agent_memories(&AgentId::new("ghost")).unwrap();
5659        assert_eq!(removed, 0);
5660    }
5661
5662    // ── All-stale recall ──────────────────────────────────────────────────────
5663
5664    #[test]
5665    fn test_recall_returns_empty_when_all_items_are_stale() {
5666        // max_age = 0.001 hours — all items inserted 1 hour ago will be evicted.
5667        let store = EpisodicStore::with_max_age(0.001).unwrap();
5668        let agent = AgentId::new("stale-agent");
5669
5670        let old_ts = Utc::now() - chrono::Duration::hours(1);
5671        store
5672            .add_episode_at(agent.clone(), "stale-1", 0.9, old_ts)
5673            .unwrap();
5674        store
5675            .add_episode_at(agent.clone(), "stale-2", 0.7, old_ts)
5676            .unwrap();
5677
5678        let items = store.recall(&agent, 100).unwrap();
5679        assert!(
5680            items.is_empty(),
5681            "all stale items should be evicted on recall, got {}",
5682            items.len()
5683        );
5684    }
5685
5686    // ── Concurrency stress tests ──────────────────────────────────────────────
5687
5688    #[test]
5689    fn test_concurrent_add_and_recall_are_consistent() {
5690        use std::sync::Arc;
5691        use std::thread;
5692
5693        let store = Arc::new(EpisodicStore::new());
5694        let agent = AgentId::new("concurrent-agent");
5695        let n_threads = 8;
5696        let items_per_thread = 25;
5697
5698        // Spawn writers.
5699        let mut handles = Vec::new();
5700        for t in 0..n_threads {
5701            let s = Arc::clone(&store);
5702            let a = agent.clone();
5703            handles.push(thread::spawn(move || {
5704                for i in 0..items_per_thread {
5705                    s.add_episode(a.clone(), format!("t{t}-i{i}"), 0.5).unwrap();
5706                }
5707            }));
5708        }
5709        for h in handles {
5710            h.join().unwrap();
5711        }
5712
5713        // Spawn readers.
5714        let mut read_handles = Vec::new();
5715        for _ in 0..n_threads {
5716            let s = Arc::clone(&store);
5717            let a = agent.clone();
5718            read_handles.push(thread::spawn(move || {
5719                let items = s.recall(&a, 1000).unwrap();
5720                assert!(items.len() <= n_threads * items_per_thread);
5721            }));
5722        }
5723        for h in read_handles {
5724            h.join().unwrap();
5725        }
5726    }
5727
5728    // ── Concurrent capacity eviction ──────────────────────────────────────────
5729
5730    #[test]
5731    fn test_concurrent_capacity_eviction_never_exceeds_cap() {
5732        use std::sync::Arc;
5733        use std::thread;
5734
5735        let cap = 5usize;
5736        let store = Arc::new(EpisodicStore::with_per_agent_capacity(cap));
5737        let agent = AgentId::new("cap-agent");
5738        let n_threads = 8;
5739        let items_per_thread = 10;
5740
5741        let mut handles = Vec::new();
5742        for t in 0..n_threads {
5743            let s = Arc::clone(&store);
5744            let a = agent.clone();
5745            handles.push(thread::spawn(move || {
5746                for i in 0..items_per_thread {
5747                    let importance = (t * items_per_thread + i) as f32 / 100.0;
5748                    s.add_episode(a.clone(), format!("t{t}-i{i}"), importance)
5749                        .unwrap();
5750                }
5751            }));
5752        }
5753        for h in handles {
5754            h.join().unwrap();
5755        }
5756
5757        // The store may momentarily exceed cap+1 during concurrent eviction,
5758        // but after all threads complete the final count must be <= cap + n_threads.
5759        // (Each thread's last insert may not have been evicted yet.)
5760        // The strong invariant: agent_memory_count <= cap + n_threads - 1.
5761        let count = store.agent_memory_count(&agent).unwrap();
5762        assert!(
5763            count <= cap + n_threads,
5764            "expected at most {} items, got {}",
5765            cap + n_threads,
5766            count
5767        );
5768    }
5769
5770    // ── SemanticStore ─────────────────────────────────────────────────────────
5771
5772    #[test]
5773    fn test_semantic_store_store_and_retrieve_all() {
5774        let store = SemanticStore::new();
5775        store.store("key1", "value1", vec!["tag-a".into()]).unwrap();
5776        store.store("key2", "value2", vec!["tag-b".into()]).unwrap();
5777        let results = store.retrieve(&[]).unwrap();
5778        assert_eq!(results.len(), 2);
5779    }
5780
5781    #[test]
5782    fn test_semantic_store_retrieve_filters_by_tag() {
5783        let store = SemanticStore::new();
5784        store
5785            .store("k1", "v1", vec!["rust".into(), "async".into()])
5786            .unwrap();
5787        store.store("k2", "v2", vec!["rust".into()]).unwrap();
5788        let results = store.retrieve(&["async"]).unwrap();
5789        assert_eq!(results.len(), 1);
5790        assert_eq!(results[0].0, "k1");
5791    }
5792
5793    #[test]
5794    fn test_semantic_store_retrieve_requires_all_tags() {
5795        let store = SemanticStore::new();
5796        store
5797            .store("k1", "v1", vec!["a".into(), "b".into()])
5798            .unwrap();
5799        store.store("k2", "v2", vec!["a".into()]).unwrap();
5800        let results = store.retrieve(&["a", "b"]).unwrap();
5801        assert_eq!(results.len(), 1);
5802    }
5803
5804    #[test]
5805    fn test_semantic_store_is_empty_initially() {
5806        let store = SemanticStore::new();
5807        assert!(store.is_empty().unwrap());
5808    }
5809
5810    #[test]
5811    fn test_semantic_store_len_tracks_insertions() {
5812        let store = SemanticStore::new();
5813        store.store("k", "v", vec![]).unwrap();
5814        assert_eq!(store.len().unwrap(), 1);
5815    }
5816
5817    #[test]
5818    fn test_semantic_store_empty_embedding_is_rejected() {
5819        let store = SemanticStore::new();
5820        let result = store.store_with_embedding("k", "v", vec![], vec![]);
5821        assert!(result.is_err(), "empty embedding should be rejected");
5822    }
5823
5824    #[test]
5825    fn test_semantic_store_dimension_mismatch_is_rejected() {
5826        let store = SemanticStore::new();
5827        store
5828            .store_with_embedding("k1", "v1", vec![], vec![1.0, 0.0])
5829            .unwrap();
5830        // Different dimension
5831        let result = store.store_with_embedding("k2", "v2", vec![], vec![1.0, 0.0, 0.0]);
5832        assert!(
5833            result.is_err(),
5834            "embedding dimension mismatch should be rejected"
5835        );
5836    }
5837
5838    #[test]
5839    fn test_semantic_store_retrieve_similar_returns_closest() {
5840        let store = SemanticStore::new();
5841        store
5842            .store_with_embedding("close", "close value", vec![], vec![1.0, 0.0, 0.0])
5843            .unwrap();
5844        store
5845            .store_with_embedding("far", "far value", vec![], vec![0.0, 1.0, 0.0])
5846            .unwrap();
5847
5848        let query = vec![1.0, 0.0, 0.0];
5849        let results = store.retrieve_similar(&query, 2).unwrap();
5850        assert_eq!(results.len(), 2);
5851        assert_eq!(results[0].0, "close");
5852        assert!(
5853            (results[0].2 - 1.0).abs() < 1e-5,
5854            "expected similarity ~1.0, got {}",
5855            results[0].2
5856        );
5857        assert!(
5858            (results[1].2).abs() < 1e-5,
5859            "expected similarity ~0.0, got {}",
5860            results[1].2
5861        );
5862    }
5863
5864    #[test]
5865    fn test_semantic_store_retrieve_similar_ignores_unembedded_entries() {
5866        let store = SemanticStore::new();
5867        store.store("no-emb", "no embedding value", vec![]).unwrap();
5868        store
5869            .store_with_embedding("with-emb", "with embedding value", vec![], vec![1.0, 0.0])
5870            .unwrap();
5871
5872        let query = vec![1.0, 0.0];
5873        let results = store.retrieve_similar(&query, 10).unwrap();
5874        assert_eq!(results.len(), 1, "only the embedded entry should appear");
5875        assert_eq!(results[0].0, "with-emb");
5876    }
5877
5878    #[test]
5879    fn test_cosine_similarity_orthogonal_vectors_return_zero() {
5880        let store = SemanticStore::new();
5881        store
5882            .store_with_embedding("a", "va", vec![], vec![1.0, 0.0])
5883            .unwrap();
5884        store
5885            .store_with_embedding("b", "vb", vec![], vec![0.0, 1.0])
5886            .unwrap();
5887
5888        let query = vec![1.0, 0.0];
5889        let results = store.retrieve_similar(&query, 2).unwrap();
5890        assert_eq!(results.len(), 2);
5891        let b_result = results.iter().find(|(k, _, _)| k == "b").unwrap();
5892        assert!(
5893            b_result.2.abs() < 1e-5,
5894            "expected cosine similarity 0.0 for orthogonal vectors, got {}",
5895            b_result.2
5896        );
5897    }
5898
5899    // ── WorkingMemory ─────────────────────────────────────────────────────────
5900
5901    #[test]
5902    fn test_working_memory_new_rejects_zero_capacity() {
5903        assert!(WorkingMemory::new(0).is_err());
5904    }
5905
5906    #[test]
5907    fn test_working_memory_set_and_get() {
5908        let wm = WorkingMemory::new(10).unwrap();
5909        wm.set("foo", "bar").unwrap();
5910        let val = wm.get("foo").unwrap();
5911        assert_eq!(val, Some("bar".into()));
5912    }
5913
5914    #[test]
5915    fn test_working_memory_get_missing_key_returns_none() {
5916        let wm = WorkingMemory::new(10).unwrap();
5917        assert_eq!(wm.get("missing").unwrap(), None);
5918    }
5919
5920    #[test]
5921    fn test_working_memory_bounded_evicts_oldest() {
5922        let wm = WorkingMemory::new(3).unwrap();
5923        wm.set("k1", "v1").unwrap();
5924        wm.set("k2", "v2").unwrap();
5925        wm.set("k3", "v3").unwrap();
5926        wm.set("k4", "v4").unwrap(); // k1 should be evicted
5927        assert_eq!(wm.get("k1").unwrap(), None);
5928        assert_eq!(wm.get("k4").unwrap(), Some("v4".into()));
5929    }
5930
5931    #[test]
5932    fn test_working_memory_update_existing_key_no_eviction() {
5933        let wm = WorkingMemory::new(2).unwrap();
5934        wm.set("k1", "v1").unwrap();
5935        wm.set("k2", "v2").unwrap();
5936        wm.set("k1", "v1-updated").unwrap(); // update, not eviction
5937        assert_eq!(wm.len().unwrap(), 2);
5938        assert_eq!(wm.get("k1").unwrap(), Some("v1-updated".into()));
5939        assert_eq!(wm.get("k2").unwrap(), Some("v2".into()));
5940    }
5941
5942    #[test]
5943    fn test_working_memory_clear_removes_all() {
5944        let wm = WorkingMemory::new(10).unwrap();
5945        wm.set("a", "1").unwrap();
5946        wm.set("b", "2").unwrap();
5947        wm.clear().unwrap();
5948        assert!(wm.is_empty().unwrap());
5949    }
5950
5951    #[test]
5952    fn test_working_memory_is_empty_initially() {
5953        let wm = WorkingMemory::new(5).unwrap();
5954        assert!(wm.is_empty().unwrap());
5955    }
5956
5957    #[test]
5958    fn test_working_memory_len_tracks_entries() {
5959        let wm = WorkingMemory::new(10).unwrap();
5960        wm.set("a", "1").unwrap();
5961        wm.set("b", "2").unwrap();
5962        assert_eq!(wm.len().unwrap(), 2);
5963    }
5964
5965    #[test]
5966    fn test_working_memory_capacity_never_exceeded() {
5967        let cap = 5usize;
5968        let wm = WorkingMemory::new(cap).unwrap();
5969        for i in 0..20 {
5970            wm.set(format!("key-{i}"), format!("val-{i}")).unwrap();
5971            assert!(wm.len().unwrap() <= cap);
5972        }
5973    }
5974
5975    // ── Improvement 6: SemanticStore dimension validation on retrieve ──────────
5976
5977    #[test]
5978    fn test_semantic_dimension_mismatch_on_retrieve_returns_error() {
5979        let store = SemanticStore::new();
5980        store
5981            .store_with_embedding("k1", "v1", vec![], vec![1.0, 0.0, 0.0])
5982            .unwrap();
5983        // Query with wrong dimension
5984        let result = store.retrieve_similar(&[1.0, 0.0], 10);
5985        assert!(result.is_err(), "dimension mismatch on retrieve should error");
5986    }
5987
5988    // ── Improvement 12: EvictionPolicy::Oldest ────────────────────────────────
5989
5990    // ── #3 clear_agent_memory ────────────────────────────────────────────────
5991
5992    #[test]
5993    fn test_clear_agent_memory_removes_all_episodes() {
5994        let store = EpisodicStore::new();
5995        let agent = AgentId::new("a");
5996        store.add_episode(agent.clone(), "ep1", 0.5).unwrap();
5997        store.add_episode(agent.clone(), "ep2", 0.9).unwrap();
5998        store.clear_agent_memory(&agent).unwrap();
5999        let items = store.recall(&agent, 10).unwrap();
6000        assert!(items.is_empty(), "all memories should be cleared");
6001    }
6002
6003    // ── #13 AgentId::as_str / MemoryId::as_str ───────────────────────────────
6004
6005    #[test]
6006    fn test_agent_id_as_str() {
6007        let id = AgentId::new("hello");
6008        assert_eq!(id.as_str(), "hello");
6009    }
6010
6011    // ── #15 export/import round trip ─────────────────────────────────────────
6012
6013    #[test]
6014    fn test_export_import_agent_memory_round_trip() {
6015        let store = EpisodicStore::new();
6016        let agent = AgentId::new("export-agent");
6017        store.add_episode(agent.clone(), "fact1", 0.8).unwrap();
6018        store.add_episode(agent.clone(), "fact2", 0.6).unwrap();
6019
6020        let exported = store.export_agent_memory(&agent).unwrap();
6021        assert_eq!(exported.len(), 2);
6022
6023        let new_store = EpisodicStore::new();
6024        new_store.import_agent_memory(&agent, exported).unwrap();
6025        let recalled = new_store.recall(&agent, 10).unwrap();
6026        assert_eq!(recalled.len(), 2);
6027    }
6028
6029    // ── #19 WorkingMemory::iter ───────────────────────────────────────────────
6030
6031    #[test]
6032    fn test_working_memory_iter_matches_entries() {
6033        let wm = WorkingMemory::new(10).unwrap();
6034        wm.set("a", "1").unwrap();
6035        wm.set("b", "2").unwrap();
6036        let via_iter = wm.iter().unwrap();
6037        let via_entries = wm.entries().unwrap();
6038        assert_eq!(via_iter, via_entries);
6039    }
6040
6041    // ── #37 AsRef<str> for AgentId and MemoryId ──────────────────────────────
6042
6043    #[test]
6044    fn test_agent_id_as_ref_str() {
6045        let id = AgentId::new("ref-test");
6046        let s: &str = id.as_ref();
6047        assert_eq!(s, "ref-test");
6048    }
6049
6050    #[test]
6051    fn test_eviction_policy_oldest_evicts_first_inserted() {
6052        let store = EpisodicStore::with_eviction_policy(EvictionPolicy::Oldest);
6053        // Override capacity by building a combined store.
6054        // We need a store with capacity=2 AND oldest eviction.
6055        // Use `with_per_agent_capacity` approach on a new store then set policy.
6056        // Since we can't combine constructors directly yet, we test via a different path:
6057        // Insert items and check that the oldest is evicted.
6058        let store = {
6059            // Build internal store with per_agent_capacity=2 and Oldest policy
6060            let inner = EpisodicInner {
6061                items: std::collections::HashMap::new(),
6062                decay: None,
6063                recall_policy: RecallPolicy::Importance,
6064                per_agent_capacity: Some(2),
6065                max_age_hours: None,
6066                eviction_policy: EvictionPolicy::Oldest,
6067            };
6068            EpisodicStore {
6069                inner: std::sync::Arc::new(std::sync::Mutex::new(inner)),
6070            }
6071        };
6072
6073        let agent = AgentId::new("agent");
6074        // Add items with distinct timestamps by using add_episode_at
6075        let t1 = chrono::Utc::now() - chrono::Duration::seconds(100);
6076        let t2 = chrono::Utc::now() - chrono::Duration::seconds(50);
6077        store.add_episode_at(agent.clone(), "oldest", 0.9, t1).unwrap();
6078        store.add_episode_at(agent.clone(), "newer", 0.8, t2).unwrap();
6079        // Adding a third item should evict "oldest" (earliest timestamp)
6080        store.add_episode(agent.clone(), "newest", 0.5).unwrap();
6081
6082        let items = store.recall(&agent, 10).unwrap();
6083        assert_eq!(items.len(), 2);
6084        let contents: Vec<&str> = items.iter().map(|i| i.content.as_str()).collect();
6085        assert!(!contents.contains(&"oldest"), "oldest item should have been evicted; got: {contents:?}");
6086    }
6087
6088    // ── New API tests (Rounds 4-8) ────────────────────────────────────────────
6089
6090    #[test]
6091    fn test_search_by_content_finds_matching_episodes() {
6092        let store = EpisodicStore::new();
6093        let agent = AgentId::new("a");
6094        store.add_episode(agent.clone(), "the quick brown fox", 0.9).unwrap();
6095        store.add_episode(agent.clone(), "jumps over the lazy dog", 0.5).unwrap();
6096        store.add_episode(agent.clone(), "hello world", 0.7).unwrap();
6097
6098        let results = store.search_by_content(&agent, "the", 10).unwrap();
6099        assert_eq!(results.len(), 2);
6100        // results are sorted by importance descending
6101        assert_eq!(results[0].content, "the quick brown fox");
6102    }
6103
6104    #[test]
6105    fn test_search_by_content_returns_empty_on_no_match() {
6106        let store = EpisodicStore::new();
6107        let agent = AgentId::new("a");
6108        store.add_episode(agent.clone(), "hello", 0.5).unwrap();
6109        let results = store.search_by_content(&agent, "xyz", 10).unwrap();
6110        assert!(results.is_empty());
6111    }
6112
6113    #[test]
6114    fn test_recall_tagged_filters_by_all_tags() {
6115        let store = EpisodicStore::new();
6116        let agent = AgentId::new("a");
6117        let mut inner = store.inner.lock().unwrap();
6118        let item1 = MemoryItem { id: MemoryId::random(), agent_id: agent.clone(), content: "rust".into(), importance: 0.8, timestamp: Utc::now(), tags: vec!["lang".into(), "sys".into()], recall_count: 0 };
6119        let item2 = MemoryItem { id: MemoryId::random(), agent_id: agent.clone(), content: "python".into(), importance: 0.6, timestamp: Utc::now(), tags: vec!["lang".into()], recall_count: 0 };
6120        inner.items.entry(agent.clone()).or_default().push(item1);
6121        inner.items.entry(agent.clone()).or_default().push(item2);
6122        drop(inner);
6123
6124        let results = store.recall_tagged(&agent, &["lang", "sys"], 10).unwrap();
6125        assert_eq!(results.len(), 1);
6126        assert_eq!(results[0].content, "rust");
6127
6128        let all = store.recall_tagged(&agent, &["lang"], 10).unwrap();
6129        assert_eq!(all.len(), 2);
6130    }
6131
6132    #[test]
6133    fn test_recall_recent_returns_newest_first() {
6134        let store = EpisodicStore::new();
6135        let agent = AgentId::new("a");
6136        store.add_episode(agent.clone(), "first", 0.3).unwrap();
6137        store.add_episode(agent.clone(), "second", 0.5).unwrap();
6138        store.add_episode(agent.clone(), "third", 0.9).unwrap();
6139
6140        let recent = store.recall_recent(&agent, 2).unwrap();
6141        assert_eq!(recent.len(), 2);
6142        assert_eq!(recent[0].content, "third");
6143        assert_eq!(recent[1].content, "second");
6144    }
6145
6146    #[test]
6147    fn test_recall_by_id_finds_specific_episode() {
6148        let store = EpisodicStore::new();
6149        let agent = AgentId::new("a");
6150        let id = store.add_episode(agent.clone(), "specific", 0.7).unwrap();
6151        store.add_episode(agent.clone(), "other", 0.5).unwrap();
6152
6153        let found = store.recall_by_id(&agent, &id).unwrap();
6154        assert!(found.is_some());
6155        assert_eq!(found.unwrap().content, "specific");
6156    }
6157
6158    #[test]
6159    fn test_recall_by_id_returns_none_for_unknown_id() {
6160        let store = EpisodicStore::new();
6161        let agent = AgentId::new("a");
6162        let result = store.recall_by_id(&agent, &MemoryId::random()).unwrap();
6163        assert!(result.is_none());
6164    }
6165
6166    #[test]
6167    fn test_update_importance_changes_score() {
6168        let store = EpisodicStore::new();
6169        let agent = AgentId::new("a");
6170        let id = store.add_episode(agent.clone(), "item", 0.5).unwrap();
6171        let updated = store.update_importance(&agent, &id, 0.9).unwrap();
6172        assert!(updated);
6173        let item = store.recall_by_id(&agent, &id).unwrap().unwrap();
6174        assert!((item.importance - 0.9).abs() < 1e-5);
6175    }
6176
6177    #[test]
6178    fn test_merge_from_imports_episodes() {
6179        let src = EpisodicStore::new();
6180        let agent = AgentId::new("a");
6181        src.add_episode(agent.clone(), "ep1", 0.8).unwrap();
6182        src.add_episode(agent.clone(), "ep2", 0.6).unwrap();
6183
6184        let dst = EpisodicStore::new();
6185        let count = dst.merge_from(&src, &agent).unwrap();
6186        assert_eq!(count, 2);
6187        assert_eq!(dst.agent_memory_count(&agent).unwrap(), 2);
6188    }
6189
6190    #[test]
6191    fn test_memory_item_age_hours_is_non_negative() {
6192        let item = MemoryItem::new(AgentId::new("a"), "test", 0.5, vec![]);
6193        let age = item.age_hours();
6194        assert!(age >= 0.0);
6195    }
6196
6197    #[test]
6198    fn test_working_memory_remove_and_contains() {
6199        let wm = WorkingMemory::new(10).unwrap();
6200        wm.set("k", "v").unwrap();
6201        assert!(wm.contains("k").unwrap());
6202        let removed = wm.remove("k").unwrap();
6203        assert!(removed);
6204        assert!(!wm.contains("k").unwrap());
6205        assert!(!wm.remove("k").unwrap()); // second remove returns false
6206    }
6207
6208    #[test]
6209    fn test_working_memory_keys_in_insertion_order() {
6210        let wm = WorkingMemory::new(10).unwrap();
6211        wm.set("b", "2").unwrap();
6212        wm.set("a", "1").unwrap();
6213        wm.set("c", "3").unwrap();
6214        assert_eq!(wm.keys().unwrap(), vec!["b", "a", "c"]);
6215    }
6216
6217    #[test]
6218    fn test_working_memory_set_many_batch_insert() {
6219        let wm = WorkingMemory::new(10).unwrap();
6220        wm.set_many([("x", "1"), ("y", "2"), ("z", "3")]).unwrap();
6221        assert_eq!(wm.len().unwrap(), 3);
6222        assert_eq!(wm.get("y").unwrap(), Some("2".into()));
6223    }
6224
6225    #[test]
6226    fn test_working_memory_get_or_default() {
6227        let wm = WorkingMemory::new(5).unwrap();
6228        wm.set("a", "val").unwrap();
6229        assert_eq!(wm.get_or_default("a", "fallback").unwrap(), "val");
6230        assert_eq!(wm.get_or_default("missing", "fallback").unwrap(), "fallback");
6231    }
6232
6233    #[test]
6234    fn test_semantic_store_remove_deletes_entry() {
6235        let store = SemanticStore::new();
6236        store.store("k", "v", vec![]).unwrap();
6237        assert_eq!(store.len().unwrap(), 1);
6238        let removed = store.remove("k").unwrap();
6239        assert!(removed);
6240        assert_eq!(store.len().unwrap(), 0);
6241    }
6242
6243    #[test]
6244    fn test_semantic_store_clear_empties_store() {
6245        let store = SemanticStore::new();
6246        store.store("a", "1", vec!["t".into()]).unwrap();
6247        store.store("b", "2", vec!["t".into()]).unwrap();
6248        store.clear().unwrap();
6249        assert!(store.is_empty().unwrap());
6250    }
6251
6252    #[test]
6253    fn test_semantic_store_update_changes_value() {
6254        let store = SemanticStore::new();
6255        store.store("k", "old", vec![]).unwrap();
6256        let updated = store.update("k", "new").unwrap();
6257        assert!(updated);
6258        let (val, _) = store.retrieve_by_key("k").unwrap().unwrap();
6259        assert_eq!(val, "new");
6260    }
6261
6262    #[test]
6263    fn test_semantic_store_retrieve_by_key() {
6264        let store = SemanticStore::new();
6265        store.store("key", "value", vec!["tag1".into()]).unwrap();
6266        let result = store.retrieve_by_key("key").unwrap().unwrap();
6267        assert_eq!(result.0, "value");
6268        assert_eq!(result.1, vec!["tag1".to_string()]);
6269        assert!(store.retrieve_by_key("missing").unwrap().is_none());
6270    }
6271
6272    #[test]
6273    fn test_semantic_store_list_tags() {
6274        let store = SemanticStore::new();
6275        store.store("a", "1", vec!["rust".into(), "sys".into()]).unwrap();
6276        store.store("b", "2", vec!["rust".into(), "ml".into()]).unwrap();
6277        let tags = store.list_tags().unwrap();
6278        assert_eq!(tags, vec!["ml", "rust", "sys"]); // sorted
6279    }
6280
6281    #[test]
6282    fn test_semantic_store_count_by_tag() {
6283        let store = SemanticStore::new();
6284        store.store("a", "1", vec!["rust".into(), "sys".into()]).unwrap();
6285        store.store("b", "2", vec!["rust".into(), "ml".into()]).unwrap();
6286        store.store("c", "3", vec!["ml".into()]).unwrap();
6287        assert_eq!(store.count_by_tag("rust").unwrap(), 2);
6288        assert_eq!(store.count_by_tag("ml").unwrap(), 2);
6289        assert_eq!(store.count_by_tag("sys").unwrap(), 1);
6290        assert_eq!(store.count_by_tag("absent").unwrap(), 0);
6291    }
6292
6293    #[test]
6294    fn test_working_memory_get_many_returns_present_values() {
6295        let wm = WorkingMemory::new(10).unwrap();
6296        wm.set("a", "alpha".to_string()).unwrap();
6297        wm.set("b", "beta".to_string()).unwrap();
6298        // get_many returns a Vec<Option<String>> in the same order as the input keys
6299        let results = wm.get_many(&["a", "b", "c"]).unwrap();
6300        assert_eq!(results[0].as_deref(), Some("alpha"));
6301        assert_eq!(results[1].as_deref(), Some("beta"));
6302        assert_eq!(results[2], None);
6303    }
6304
6305    #[test]
6306    fn test_working_memory_update_if_exists_changes_value() {
6307        let wm = WorkingMemory::new(5).unwrap();
6308        wm.set("x", "old".to_string()).unwrap();
6309        assert!(wm.update_if_exists("x", "new".to_string()).unwrap());
6310        assert_eq!(wm.get("x").unwrap().as_deref(), Some("new"));
6311    }
6312
6313    #[test]
6314    fn test_working_memory_update_if_exists_returns_false_for_missing() {
6315        let wm = WorkingMemory::new(5).unwrap();
6316        assert!(!wm.update_if_exists("nope", "val".to_string()).unwrap());
6317    }
6318
6319    #[test]
6320    fn test_episodic_total_recall_count_sums_all_accesses() {
6321        let store = EpisodicStore::new();
6322        let agent = AgentId::new("agent1");
6323        store.add_episode(agent.clone(), "first", 0.8).unwrap();
6324        store.add_episode(agent.clone(), "second", 0.5).unwrap();
6325        // recall() increments recall_count on each returned item
6326        store.recall(&agent, 10).unwrap();
6327        store.recall(&agent, 10).unwrap();
6328        let total = store.total_recall_count(&agent).unwrap();
6329        // 2 items × 2 recalls = 4
6330        assert_eq!(total, 4);
6331    }
6332
6333    #[test]
6334    fn test_episodic_total_recall_count_zero_before_recall() {
6335        let store = EpisodicStore::new();
6336        let agent = AgentId::new("fresh");
6337        store.add_episode(agent.clone(), "ep", 0.5).unwrap();
6338        assert_eq!(store.total_recall_count(&agent).unwrap(), 0);
6339    }
6340
6341    #[test]
6342    fn test_episodic_recall_all_returns_all_items() {
6343        let store = EpisodicStore::new();
6344        let agent = AgentId::new("agent-all");
6345        store.add_episode(agent.clone(), "one", 0.9).unwrap();
6346        store.add_episode(agent.clone(), "two", 0.5).unwrap();
6347        store.add_episode(agent.clone(), "three", 0.1).unwrap();
6348        let all = store.recall_all(&agent).unwrap();
6349        assert_eq!(all.len(), 3);
6350    }
6351
6352    #[test]
6353    fn test_episodic_recall_all_empty_agent_returns_empty() {
6354        let store = EpisodicStore::new();
6355        let agent = AgentId::new("nobody");
6356        let all = store.recall_all(&agent).unwrap();
6357        assert!(all.is_empty());
6358    }
6359
6360    #[test]
6361    fn test_episodic_clear_all_removes_all_agents() {
6362        let store = EpisodicStore::new();
6363        let a1 = AgentId::new("a1");
6364        let a2 = AgentId::new("a2");
6365        store.add_episode(a1.clone(), "ep", 0.5).unwrap();
6366        store.add_episode(a2.clone(), "ep", 0.5).unwrap();
6367        store.clear_all().unwrap();
6368        assert_eq!(store.len().unwrap(), 0);
6369        assert!(store.list_agents().unwrap().is_empty());
6370    }
6371
6372    #[test]
6373    fn test_working_memory_drain_returns_all_and_empties() {
6374        let wm = WorkingMemory::new(10).unwrap();
6375        wm.set("x", "1".to_string()).unwrap();
6376        wm.set("y", "2".to_string()).unwrap();
6377        let drained = wm.drain().unwrap();
6378        assert_eq!(drained.len(), 2);
6379        assert!(wm.is_empty().unwrap());
6380    }
6381
6382    #[test]
6383    fn test_working_memory_drain_preserves_insertion_order() {
6384        let wm = WorkingMemory::new(10).unwrap();
6385        wm.set("a", "1".to_string()).unwrap();
6386        wm.set("b", "2".to_string()).unwrap();
6387        wm.set("c", "3".to_string()).unwrap();
6388        let drained = wm.drain().unwrap();
6389        let keys: Vec<&str> = drained.iter().map(|(k, _)| k.as_str()).collect();
6390        assert_eq!(keys, vec!["a", "b", "c"]);
6391    }
6392
6393    #[test]
6394    fn test_semantic_store_tag_count() {
6395        let store = SemanticStore::new();
6396        store.store("a", "1", vec!["rust".into(), "sys".into()]).unwrap();
6397        store.store("b", "2", vec!["rust".into(), "ml".into()]).unwrap();
6398        assert_eq!(store.tag_count().unwrap(), 3); // rust, sys, ml
6399    }
6400
6401    #[test]
6402    fn test_semantic_store_tag_count_empty() {
6403        let store = SemanticStore::new();
6404        assert_eq!(store.tag_count().unwrap(), 0);
6405    }
6406
6407    #[test]
6408    fn test_episodic_top_n_returns_highest_importance() {
6409        let store = EpisodicStore::new();
6410        let agent = AgentId::new("ag");
6411        store.add_episode(agent.clone(), "high", 0.9).unwrap();
6412        store.add_episode(agent.clone(), "med", 0.5).unwrap();
6413        store.add_episode(agent.clone(), "low", 0.1).unwrap();
6414        let top = store.top_n(&agent, 2).unwrap();
6415        assert_eq!(top.len(), 2);
6416        assert_eq!(top[0].content, "high");
6417        assert_eq!(top[1].content, "med");
6418    }
6419
6420    #[test]
6421    fn test_episodic_top_n_zero_returns_all() {
6422        let store = EpisodicStore::new();
6423        let agent = AgentId::new("ag");
6424        store.add_episode(agent.clone(), "a", 0.9).unwrap();
6425        store.add_episode(agent.clone(), "b", 0.5).unwrap();
6426        assert_eq!(store.top_n(&agent, 0).unwrap().len(), 2);
6427    }
6428
6429    #[test]
6430    fn test_episodic_search_by_importance_range() {
6431        let store = EpisodicStore::new();
6432        let agent = AgentId::new("ag");
6433        store.add_episode(agent.clone(), "high", 0.9).unwrap();
6434        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
6435        store.add_episode(agent.clone(), "low", 0.1).unwrap();
6436        let results = store.search_by_importance_range(&agent, 0.4, 1.0, 0).unwrap();
6437        assert_eq!(results.len(), 2);
6438        assert_eq!(results[0].content, "high");
6439    }
6440
6441    #[test]
6442    fn test_semantic_store_contains_true_for_stored_key() {
6443        let store = SemanticStore::new();
6444        store.store("k", "v", vec![]).unwrap();
6445        assert!(store.contains("k").unwrap());
6446    }
6447
6448    #[test]
6449    fn test_semantic_store_contains_false_for_missing_key() {
6450        let store = SemanticStore::new();
6451        assert!(!store.contains("missing").unwrap());
6452    }
6453
6454    #[test]
6455    fn test_working_memory_rename_changes_key() {
6456        let wm = WorkingMemory::new(10).unwrap();
6457        wm.set("old", "value".to_string()).unwrap();
6458        assert!(wm.rename("old", "new").unwrap());
6459        assert_eq!(wm.get("new").unwrap().as_deref(), Some("value"));
6460        assert!(wm.get("old").unwrap().is_none());
6461    }
6462
6463    #[test]
6464    fn test_working_memory_rename_preserves_order() {
6465        let wm = WorkingMemory::new(10).unwrap();
6466        wm.set("a", "1".to_string()).unwrap();
6467        wm.set("b", "2".to_string()).unwrap();
6468        wm.rename("a", "x").unwrap();
6469        assert_eq!(wm.keys().unwrap(), vec!["x", "b"]);
6470    }
6471
6472    #[test]
6473    fn test_working_memory_rename_missing_returns_false() {
6474        let wm = WorkingMemory::new(10).unwrap();
6475        assert!(!wm.rename("nope", "other").unwrap());
6476    }
6477
6478    #[test]
6479    fn test_episodic_importance_stats_basic() {
6480        let store = EpisodicStore::new();
6481        let agent = AgentId::new("ag");
6482        store.add_episode(agent.clone(), "a", 0.2).unwrap();
6483        store.add_episode(agent.clone(), "b", 0.6).unwrap();
6484        store.add_episode(agent.clone(), "c", 1.0).unwrap();
6485        let (count, min, max, mean) = store.importance_stats(&agent).unwrap();
6486        assert_eq!(count, 3);
6487        assert!((min - 0.2).abs() < 1e-5);
6488        assert!((max - 1.0).abs() < 1e-5);
6489        assert!((mean - 0.6).abs() < 1e-4);
6490    }
6491
6492    #[test]
6493    fn test_episodic_importance_stats_empty_agent() {
6494        let store = EpisodicStore::new();
6495        let agent = AgentId::new("nobody");
6496        let (count, min, max, mean) = store.importance_stats(&agent).unwrap();
6497        assert_eq!(count, 0);
6498        assert_eq!(min, 0.0);
6499        assert_eq!(max, 0.0);
6500        assert_eq!(mean, 0.0);
6501    }
6502
6503    #[test]
6504    fn test_semantic_store_keys_returns_stored_keys() {
6505        let store = SemanticStore::new();
6506        store.store("alpha", "v1", vec![]).unwrap();
6507        store.store("beta", "v2", vec![]).unwrap();
6508        let keys = store.keys().unwrap();
6509        assert_eq!(keys.len(), 2);
6510        assert!(keys.contains(&"alpha".to_string()));
6511        assert!(keys.contains(&"beta".to_string()));
6512    }
6513
6514    #[test]
6515    fn test_working_memory_snapshot_clones_contents() {
6516        let wm = WorkingMemory::new(10).unwrap();
6517        wm.set("x", "1".to_string()).unwrap();
6518        wm.set("y", "2".to_string()).unwrap();
6519        let snap = wm.snapshot().unwrap();
6520        assert_eq!(snap.get("x").map(|s| s.as_str()), Some("1"));
6521        assert_eq!(snap.get("y").map(|s| s.as_str()), Some("2"));
6522        // Memory still has items
6523        assert_eq!(wm.len().unwrap(), 2);
6524    }
6525
6526    // ── Round 3: new methods ──────────────────────────────────────────────────
6527
6528    #[test]
6529    fn test_memory_item_display_includes_content() {
6530        let agent = AgentId::new("a");
6531        let item = MemoryItem::new(agent, "hello world", 0.75, vec![]);
6532        let s = format!("{item}");
6533        assert!(s.contains("hello world"), "Display should include content");
6534        assert!(s.contains("0.75"), "Display should include importance");
6535    }
6536
6537    #[test]
6538    fn test_decay_policy_half_life_hours_accessor() {
6539        let policy = DecayPolicy::exponential(12.5).unwrap();
6540        assert!((policy.half_life_hours() - 12.5).abs() < f64::EPSILON);
6541    }
6542
6543    #[test]
6544    fn test_semantic_store_list_keys_returns_all_keys() {
6545        let store = SemanticStore::new();
6546        store.store("k1", "v1", vec![]).unwrap();
6547        store.store("k2", "v2", vec![]).unwrap();
6548        let keys = store.list_keys().unwrap();
6549        assert_eq!(keys.len(), 2);
6550        assert!(keys.contains(&"k1".to_string()));
6551        assert!(keys.contains(&"k2".to_string()));
6552    }
6553
6554    #[test]
6555    fn test_semantic_store_update_tags_returns_true_on_found() {
6556        let store = SemanticStore::new();
6557        store.store("k", "v", vec!["old".into()]).unwrap();
6558        let updated = store.update_tags("k", vec!["new".into()]).unwrap();
6559        assert!(updated);
6560        let (_, tags) = store.retrieve_by_key("k").unwrap().unwrap();
6561        assert_eq!(tags, vec!["new".to_string()]);
6562    }
6563
6564    #[test]
6565    fn test_semantic_store_update_tags_returns_false_on_missing() {
6566        let store = SemanticStore::new();
6567        assert!(!store.update_tags("missing", vec![]).unwrap());
6568    }
6569
6570    #[test]
6571    fn test_episodic_store_recall_since_filters_old() {
6572        let store = EpisodicStore::new();
6573        let agent = AgentId::new("a");
6574        let old_ts = Utc::now() - chrono::Duration::hours(2);
6575        store.add_episode_at(agent.clone(), "old", 0.9, old_ts).unwrap();
6576        store.add_episode(agent.clone(), "new", 0.5).unwrap();
6577
6578        let cutoff = Utc::now() - chrono::Duration::minutes(30);
6579        let items = store.recall_since(&agent, cutoff, 0).unwrap();
6580        assert_eq!(items.len(), 1);
6581        assert_eq!(items[0].content, "new");
6582    }
6583
6584    #[test]
6585    fn test_episodic_store_recall_since_limit_respected() {
6586        let store = EpisodicStore::new();
6587        let agent = AgentId::new("a");
6588        for i in 0..5 {
6589            store.add_episode(agent.clone(), format!("item {i}"), 0.5).unwrap();
6590        }
6591        let cutoff = Utc::now() - chrono::Duration::hours(1);
6592        let items = store.recall_since(&agent, cutoff, 2).unwrap();
6593        assert_eq!(items.len(), 2);
6594    }
6595
6596    #[test]
6597    fn test_episodic_store_update_content_returns_true_on_found() {
6598        let store = EpisodicStore::new();
6599        let agent = AgentId::new("a");
6600        let id = store.add_episode(agent.clone(), "original", 0.5).unwrap();
6601        let updated = store.update_content(&agent, &id, "updated").unwrap();
6602        assert!(updated);
6603        let item = store.recall_by_id(&agent, &id).unwrap().unwrap();
6604        assert_eq!(item.content, "updated");
6605    }
6606
6607    #[test]
6608    fn test_episodic_store_update_content_returns_false_on_missing() {
6609        let store = EpisodicStore::new();
6610        let agent = AgentId::new("a");
6611        let fake_id = MemoryId::new("does-not-exist");
6612        assert!(!store.update_content(&agent, &fake_id, "x").unwrap());
6613    }
6614
6615    #[test]
6616    fn test_working_memory_capacity_matches_constructor() {
6617        let wm = WorkingMemory::new(7).unwrap();
6618        assert_eq!(wm.capacity(), 7);
6619    }
6620
6621    // ── Round 4: EpisodicStore new helpers ───────────────────────────────────
6622
6623    #[test]
6624    fn test_add_episode_with_tags_stores_and_recalls() {
6625        let store = EpisodicStore::new();
6626        let agent = AgentId::new("a");
6627        let id = store
6628            .add_episode_with_tags(agent.clone(), "tagged", 0.8, vec!["t1".to_string(), "t2".to_string()])
6629            .unwrap();
6630        let item = store.recall_by_id(&agent, &id).unwrap().unwrap();
6631        assert_eq!(item.content, "tagged");
6632        assert_eq!(item.tags, vec!["t1", "t2"]);
6633    }
6634
6635    #[test]
6636    fn test_remove_by_id_deletes_episode() {
6637        let store = EpisodicStore::new();
6638        let agent = AgentId::new("a");
6639        let id = store.add_episode(agent.clone(), "to-delete", 0.5).unwrap();
6640        assert!(store.remove_by_id(&agent, &id).unwrap());
6641        assert!(store.recall_by_id(&agent, &id).unwrap().is_none());
6642    }
6643
6644    #[test]
6645    fn test_remove_by_id_returns_false_for_missing() {
6646        let store = EpisodicStore::new();
6647        let agent = AgentId::new("a");
6648        let fake = MemoryId::new("does-not-exist");
6649        assert!(!store.remove_by_id(&agent, &fake).unwrap());
6650    }
6651
6652    #[test]
6653    fn test_update_tags_by_id_replaces_tags() {
6654        let store = EpisodicStore::new();
6655        let agent = AgentId::new("a");
6656        let id = store
6657            .add_episode_with_tags(agent.clone(), "x", 0.5, vec!["old".to_string()])
6658            .unwrap();
6659        let updated = store
6660            .update_tags_by_id(&agent, &id, vec!["new1".to_string(), "new2".to_string()])
6661            .unwrap();
6662        assert!(updated);
6663        let item = store.recall_by_id(&agent, &id).unwrap().unwrap();
6664        assert_eq!(item.tags, vec!["new1", "new2"]);
6665    }
6666
6667    #[test]
6668    fn test_update_tags_by_id_returns_false_for_missing() {
6669        let store = EpisodicStore::new();
6670        let agent = AgentId::new("a");
6671        let fake = MemoryId::new("nope");
6672        assert!(!store.update_tags_by_id(&agent, &fake, vec![]).unwrap());
6673    }
6674
6675    #[test]
6676    fn test_max_importance_for_returns_highest() {
6677        let store = EpisodicStore::new();
6678        let agent = AgentId::new("a");
6679        store.add_episode(agent.clone(), "low", 0.2).unwrap();
6680        store.add_episode(agent.clone(), "high", 0.9).unwrap();
6681        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
6682        let max = store.max_importance_for(&agent).unwrap().unwrap();
6683        assert!((max - 0.9).abs() < 1e-6);
6684    }
6685
6686    #[test]
6687    fn test_max_importance_for_returns_none_when_empty() {
6688        let store = EpisodicStore::new();
6689        let agent = AgentId::new("empty");
6690        assert!(store.max_importance_for(&agent).unwrap().is_none());
6691    }
6692
6693    // ── Round 4: WorkingMemory::snapshot / pop_oldest ────────────────────────
6694
6695    #[test]
6696    fn test_working_memory_snapshot_returns_all_entries() {
6697        let wm = WorkingMemory::new(10).unwrap();
6698        wm.set("k1", "v1").unwrap();
6699        wm.set("k2", "v2").unwrap();
6700        let snap = wm.snapshot().unwrap();
6701        assert_eq!(snap.len(), 2);
6702        assert_eq!(snap.get("k1").unwrap(), "v1");
6703        assert_eq!(snap.get("k2").unwrap(), "v2");
6704    }
6705
6706    #[test]
6707    fn test_working_memory_pop_oldest_removes_first_inserted() {
6708        let wm = WorkingMemory::new(10).unwrap();
6709        wm.set("first", "1").unwrap();
6710        wm.set("second", "2").unwrap();
6711        let popped = wm.pop_oldest().unwrap().unwrap();
6712        assert_eq!(popped.0, "first");
6713        assert_eq!(popped.1, "1");
6714        assert_eq!(wm.len().unwrap(), 1);
6715    }
6716
6717    #[test]
6718    fn test_working_memory_pop_oldest_returns_none_when_empty() {
6719        let wm = WorkingMemory::new(5).unwrap();
6720        assert!(wm.pop_oldest().unwrap().is_none());
6721    }
6722
6723    // ── Round 4: SemanticStore::keys_with_tag ────────────────────────────────
6724
6725    #[test]
6726    fn test_semantic_store_keys_with_tag_returns_matching() {
6727        let store = SemanticStore::new();
6728        store
6729            .store("doc1", "content one", vec!["alpha".to_string(), "beta".to_string()])
6730            .unwrap();
6731        store
6732            .store("doc2", "content two", vec!["alpha".to_string()])
6733            .unwrap();
6734        store
6735            .store("doc3", "content three", vec!["gamma".to_string()])
6736            .unwrap();
6737        let mut keys = store.keys_with_tag("alpha").unwrap();
6738        keys.sort();
6739        assert_eq!(keys, vec!["doc1", "doc2"]);
6740    }
6741
6742    #[test]
6743    fn test_semantic_store_keys_with_tag_empty_when_no_match() {
6744        let store = SemanticStore::new();
6745        store
6746            .store("doc1", "content", vec!["x".to_string()])
6747            .unwrap();
6748        let keys = store.keys_with_tag("z").unwrap();
6749        assert!(keys.is_empty());
6750    }
6751
6752    // ── Round 16: oldest, get_or_default ─────────────────────────────────────
6753
6754    #[test]
6755    fn test_episodic_oldest_returns_first_inserted() {
6756        let store = EpisodicStore::new();
6757        let agent = AgentId::new("agent-oldest");
6758        store.add_episode(agent.clone(), "first episode", 0.5).unwrap();
6759        store.add_episode(agent.clone(), "second episode", 0.9).unwrap();
6760        let oldest = store.oldest(&agent).unwrap().unwrap();
6761        assert_eq!(oldest.content, "first episode");
6762    }
6763
6764    #[test]
6765    fn test_episodic_oldest_none_when_no_episodes() {
6766        let store = EpisodicStore::new();
6767        let agent = AgentId::new("no-episodes");
6768        assert!(store.oldest(&agent).unwrap().is_none());
6769    }
6770
6771    #[test]
6772    fn test_working_memory_get_or_default_returns_value_when_present() {
6773        let wm = WorkingMemory::new(10).unwrap();
6774        wm.set("key", "value").unwrap();
6775        assert_eq!(wm.get_or_default("key", "fallback").unwrap(), "value");
6776    }
6777
6778    #[test]
6779    fn test_working_memory_get_or_default_returns_default_when_absent() {
6780        let wm = WorkingMemory::new(10).unwrap();
6781        assert_eq!(wm.get_or_default("missing", "fallback").unwrap(), "fallback");
6782    }
6783
6784    // ── Round 5: EpisodicStore new helpers ───────────────────────────────────
6785
6786    #[test]
6787    fn test_episodic_count_for_returns_correct_count() {
6788        let store = EpisodicStore::new();
6789        let agent = AgentId::new("a");
6790        store.add_episode(agent.clone(), "e1", 0.5).unwrap();
6791        store.add_episode(agent.clone(), "e2", 0.5).unwrap();
6792        assert_eq!(store.count_for(&agent).unwrap(), 2);
6793    }
6794
6795    #[test]
6796    fn test_episodic_count_for_returns_zero_for_unknown_agent() {
6797        let store = EpisodicStore::new();
6798        let agent = AgentId::new("unknown");
6799        assert_eq!(store.count_for(&agent).unwrap(), 0);
6800    }
6801
6802    #[test]
6803    fn test_recall_by_tag_returns_matching_sorted_by_importance() {
6804        let store = EpisodicStore::new();
6805        let agent = AgentId::new("a");
6806        store
6807            .add_episode_with_tags(agent.clone(), "low", 0.1, vec!["news".to_string()])
6808            .unwrap();
6809        store
6810            .add_episode_with_tags(agent.clone(), "high", 0.9, vec!["news".to_string()])
6811            .unwrap();
6812        store
6813            .add_episode_with_tags(agent.clone(), "other", 0.8, vec!["other".to_string()])
6814            .unwrap();
6815        let items = store.recall_by_tag(&agent, "news", 0).unwrap();
6816        assert_eq!(items.len(), 2);
6817        assert_eq!(items[0].content, "high");
6818    }
6819
6820    #[test]
6821    fn test_recall_by_tag_respects_limit() {
6822        let store = EpisodicStore::new();
6823        let agent = AgentId::new("a");
6824        for i in 0..5 {
6825            store
6826                .add_episode_with_tags(agent.clone(), format!("ep{i}"), 0.5, vec!["t".to_string()])
6827                .unwrap();
6828        }
6829        let items = store.recall_by_tag(&agent, "t", 2).unwrap();
6830        assert_eq!(items.len(), 2);
6831    }
6832
6833    #[test]
6834    fn test_merge_from_copies_episodes() {
6835        let src = EpisodicStore::new();
6836        let dst = EpisodicStore::new();
6837        let agent = AgentId::new("a");
6838        src.add_episode(agent.clone(), "ep1", 0.5).unwrap();
6839        src.add_episode(agent.clone(), "ep2", 0.9).unwrap();
6840        let count = dst.merge_from(&src, &agent).unwrap();
6841        assert_eq!(count, 2);
6842        assert_eq!(dst.count_for(&agent).unwrap(), 2);
6843    }
6844
6845    // ── Round 5: WorkingMemory get_all_keys / replace_all ────────────────────
6846
6847    #[test]
6848    fn test_working_memory_get_all_keys_preserves_insertion_order() {
6849        let wm = WorkingMemory::new(10).unwrap();
6850        wm.set("first", "1").unwrap();
6851        wm.set("second", "2").unwrap();
6852        wm.set("third", "3").unwrap();
6853        assert_eq!(wm.get_all_keys().unwrap(), vec!["first", "second", "third"]);
6854    }
6855
6856    #[test]
6857    fn test_working_memory_replace_all_replaces_contents() {
6858        let wm = WorkingMemory::new(10).unwrap();
6859        wm.set("old", "x").unwrap();
6860        let mut new_map = std::collections::HashMap::new();
6861        new_map.insert("a".to_string(), "1".to_string());
6862        new_map.insert("b".to_string(), "2".to_string());
6863        wm.replace_all(new_map).unwrap();
6864        assert_eq!(wm.len().unwrap(), 2);
6865        assert!(wm.get("old").unwrap().is_none());
6866        assert_eq!(wm.get("a").unwrap().unwrap(), "1");
6867    }
6868
6869    // ── Round 5: SemanticStore::remove / count ───────────────────────────────
6870
6871    #[test]
6872    fn test_semantic_store_remove_returns_true_and_removes() {
6873        let store = SemanticStore::new();
6874        store.store("k1", "v1", vec![]).unwrap();
6875        store.store("k2", "v2", vec![]).unwrap();
6876        assert!(store.remove("k1").unwrap());
6877        assert_eq!(store.count().unwrap(), 1);
6878        let keys = store.list_keys().unwrap();
6879        assert_eq!(keys, vec!["k2"]);
6880    }
6881
6882    #[test]
6883    fn test_semantic_store_remove_returns_false_when_missing() {
6884        let store = SemanticStore::new();
6885        assert!(!store.remove("ghost").unwrap());
6886    }
6887
6888    #[test]
6889    fn test_semantic_store_count_matches_len() {
6890        let store = SemanticStore::new();
6891        store.store("a", "1", vec![]).unwrap();
6892        store.store("b", "2", vec![]).unwrap();
6893        assert_eq!(store.count().unwrap(), store.len().unwrap());
6894    }
6895
6896    // ── Round 17: newest, values ─────────────────────────────────────────────
6897
6898    #[test]
6899    fn test_episodic_newest_returns_last_inserted() {
6900        let store = EpisodicStore::new();
6901        let agent = AgentId::new("agent-newest");
6902        store.add_episode(agent.clone(), "first", 0.3).unwrap();
6903        store.add_episode(agent.clone(), "last", 0.8).unwrap();
6904        let newest = store.newest(&agent).unwrap().unwrap();
6905        assert_eq!(newest.content, "last");
6906    }
6907
6908    #[test]
6909    fn test_episodic_newest_none_when_empty() {
6910        let store = EpisodicStore::new();
6911        let agent = AgentId::new("no-ep");
6912        assert!(store.newest(&agent).unwrap().is_none());
6913    }
6914
6915    #[test]
6916    fn test_working_memory_values_returns_values_in_insertion_order() {
6917        let wm = WorkingMemory::new(10).unwrap();
6918        wm.set("a", "apple").unwrap();
6919        wm.set("b", "banana").unwrap();
6920        let vals = wm.values().unwrap();
6921        assert_eq!(vals, vec!["apple", "banana"]);
6922    }
6923
6924    #[test]
6925    fn test_working_memory_values_empty_when_no_entries() {
6926        let wm = WorkingMemory::new(10).unwrap();
6927        assert!(wm.values().unwrap().is_empty());
6928    }
6929
6930    // ── Round 18: clear_agent ────────────────────────────────────────────────
6931
6932    #[test]
6933    fn test_episodic_clear_agent_removes_all_episodes() {
6934        let store = EpisodicStore::new();
6935        let agent = AgentId::new("agent-clear");
6936        store.add_episode(agent.clone(), "ep1", 0.5).unwrap();
6937        store.add_episode(agent.clone(), "ep2", 0.7).unwrap();
6938        let removed = store.clear_agent(&agent).unwrap();
6939        assert_eq!(removed, 2);
6940        assert_eq!(store.agent_memory_count(&agent).unwrap(), 0);
6941    }
6942
6943    #[test]
6944    fn test_episodic_clear_agent_returns_zero_when_none() {
6945        let store = EpisodicStore::new();
6946        let agent = AgentId::new("no-agent");
6947        assert_eq!(store.clear_agent(&agent).unwrap(), 0);
6948    }
6949
6950    // ── Round 19: max_importance_episode, SemanticStore::update ──────────────
6951
6952    #[test]
6953    fn test_episodic_max_importance_episode_returns_highest() {
6954        let store = EpisodicStore::new();
6955        let agent = AgentId::new("agent-max");
6956        store.add_episode(agent.clone(), "low", 0.2).unwrap();
6957        store.add_episode(agent.clone(), "high", 0.9).unwrap();
6958        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
6959        let top = store.max_importance_episode(&agent).unwrap().unwrap();
6960        assert_eq!(top.content, "high");
6961    }
6962
6963    #[test]
6964    fn test_episodic_max_importance_episode_none_when_empty() {
6965        let store = EpisodicStore::new();
6966        let agent = AgentId::new("empty-max");
6967        assert!(store.max_importance_episode(&agent).unwrap().is_none());
6968    }
6969
6970    #[test]
6971    fn test_semantic_store_update_replaces_value() {
6972        let store = SemanticStore::new();
6973        store.store("key1", "original", vec![]).unwrap();
6974        let updated = store.update("key1", "new value").unwrap();
6975        assert!(updated);
6976        let retrieved = store.retrieve_by_key("key1").unwrap().unwrap();
6977        assert_eq!(retrieved.0, "new value");
6978    }
6979
6980    #[test]
6981    fn test_semantic_store_update_returns_false_for_missing_key() {
6982        let store = SemanticStore::new();
6983        assert!(!store.update("missing", "value").unwrap());
6984    }
6985
6986    // ── Round 6: EpisodicStore::clear_for / importance_sum ───────────────────
6987
6988    #[test]
6989    fn test_episodic_clear_for_removes_all_episodes_for_agent() {
6990        let store = EpisodicStore::new();
6991        let agent = AgentId::new("a");
6992        store.add_episode(agent.clone(), "e1", 0.5).unwrap();
6993        store.add_episode(agent.clone(), "e2", 0.9).unwrap();
6994        let removed = store.clear_for(&agent).unwrap();
6995        assert_eq!(removed, 2);
6996        assert_eq!(store.count_for(&agent).unwrap(), 0);
6997    }
6998
6999    #[test]
7000    fn test_episodic_clear_for_returns_zero_for_unknown_agent() {
7001        let store = EpisodicStore::new();
7002        let agent = AgentId::new("ghost");
7003        assert_eq!(store.clear_for(&agent).unwrap(), 0);
7004    }
7005
7006    #[test]
7007    fn test_episodic_importance_sum_correct() {
7008        let store = EpisodicStore::new();
7009        let agent = AgentId::new("a");
7010        store.add_episode(agent.clone(), "e1", 0.3).unwrap();
7011        store.add_episode(agent.clone(), "e2", 0.5).unwrap();
7012        let sum = store.importance_sum(&agent).unwrap();
7013        assert!((sum - 0.8).abs() < 1e-5);
7014    }
7015
7016    #[test]
7017    fn test_episodic_importance_sum_zero_when_empty() {
7018        let store = EpisodicStore::new();
7019        let agent = AgentId::new("empty");
7020        assert_eq!(store.importance_sum(&agent).unwrap(), 0.0);
7021    }
7022
7023    // ── Round 6: WorkingMemory::merge_from / entry_count_satisfying ──────────
7024
7025    #[test]
7026    fn test_working_memory_merge_from_copies_entries() {
7027        let src = WorkingMemory::new(10).unwrap();
7028        src.set("x", "1").unwrap();
7029        src.set("y", "2").unwrap();
7030        let dst = WorkingMemory::new(10).unwrap();
7031        let count = dst.merge_from(&src).unwrap();
7032        assert_eq!(count, 2);
7033        assert_eq!(dst.get("x").unwrap().unwrap(), "1");
7034        assert_eq!(dst.get("y").unwrap().unwrap(), "2");
7035    }
7036
7037    #[test]
7038    fn test_working_memory_entry_count_satisfying_counts_matching() {
7039        let wm = WorkingMemory::new(10).unwrap();
7040        wm.set("a", "hello").unwrap();
7041        wm.set("b", "world").unwrap();
7042        wm.set("c", "hello world").unwrap();
7043        let count = wm
7044            .entry_count_satisfying(|_, v| v.contains("hello"))
7045            .unwrap();
7046        assert_eq!(count, 2);
7047    }
7048
7049    // ── Round 6: SemanticStore::update_value ─────────────────────────────────
7050
7051    #[test]
7052    fn test_semantic_update_value_replaces_stored_value() {
7053        let store = SemanticStore::new();
7054        store.store("k", "old", vec![]).unwrap();
7055        assert!(store.update_value("k", "new").unwrap());
7056        let pairs = store.retrieve(&[]).unwrap();
7057        assert!(pairs.iter().any(|(_, v)| v == "new"));
7058    }
7059
7060    #[test]
7061    fn test_semantic_update_value_returns_false_when_missing() {
7062        let store = SemanticStore::new();
7063        assert!(!store.update_value("nope", "x").unwrap());
7064    }
7065
7066    // ── Round 7: EpisodicStore::agent_ids / find_by_content ──────────────────
7067
7068    #[test]
7069    fn test_episodic_agent_ids_returns_all_agents() {
7070        let store = EpisodicStore::new();
7071        let a1 = AgentId::new("agent-1");
7072        let a2 = AgentId::new("agent-2");
7073        store.add_episode(a1.clone(), "e", 0.5).unwrap();
7074        store.add_episode(a2.clone(), "e", 0.5).unwrap();
7075        let mut ids = store.agent_ids().unwrap();
7076        ids.sort_by_key(|id| id.0.clone());
7077        assert_eq!(ids, vec![a1, a2]);
7078    }
7079
7080    #[test]
7081    fn test_episodic_agent_ids_empty_for_new_store() {
7082        let store = EpisodicStore::new();
7083        assert!(store.agent_ids().unwrap().is_empty());
7084    }
7085
7086    #[test]
7087    fn test_find_by_content_returns_matching_episodes() {
7088        let store = EpisodicStore::new();
7089        let agent = AgentId::new("a");
7090        store.add_episode(agent.clone(), "hello world", 0.5).unwrap();
7091        store.add_episode(agent.clone(), "goodbye world", 0.8).unwrap();
7092        store.add_episode(agent.clone(), "something else", 0.9).unwrap();
7093        let results = store.find_by_content(&agent, "world").unwrap();
7094        assert_eq!(results.len(), 2);
7095        // sorted by descending importance
7096        assert_eq!(results[0].content, "goodbye world");
7097    }
7098
7099    #[test]
7100    fn test_find_by_content_returns_empty_when_no_match() {
7101        let store = EpisodicStore::new();
7102        let agent = AgentId::new("a");
7103        store.add_episode(agent.clone(), "hello", 0.5).unwrap();
7104        let results = store.find_by_content(&agent, "xyz").unwrap();
7105        assert!(results.is_empty());
7106    }
7107
7108    // ── Round 20: add_episode_at / add_episodes_batch / SemanticStore::keys_matching ──
7109
7110    #[test]
7111    fn test_add_episode_at_stores_with_given_timestamp() {
7112        let store = EpisodicStore::new();
7113        let agent = AgentId::new("agent-ts");
7114        let ts = chrono::Utc::now() - chrono::Duration::hours(2);
7115        let id = store.add_episode_at(agent.clone(), "past event", 0.7, ts).unwrap();
7116        let items = store.recall_all(&agent).unwrap();
7117        assert_eq!(items.len(), 1);
7118        assert_eq!(items[0].id, id);
7119        assert_eq!(items[0].content, "past event");
7120        assert!((items[0].timestamp - ts).num_seconds().abs() < 1);
7121    }
7122
7123    #[test]
7124    fn test_add_episodes_batch_returns_all_ids() {
7125        let store = EpisodicStore::new();
7126        let agent = AgentId::new("batch-agent");
7127        let episodes = vec![
7128            ("first", 0.5f32),
7129            ("second", 0.8f32),
7130            ("third", 0.3f32),
7131        ];
7132        let ids = store.add_episodes_batch(agent.clone(), episodes).unwrap();
7133        assert_eq!(ids.len(), 3);
7134        let all = store.recall_all(&agent).unwrap();
7135        assert_eq!(all.len(), 3);
7136    }
7137
7138    #[test]
7139    fn test_add_episodes_batch_empty_iter_returns_empty_ids() {
7140        let store = EpisodicStore::new();
7141        let agent = AgentId::new("empty-batch");
7142        let ids = store
7143            .add_episodes_batch(agent.clone(), Vec::<(String, f32)>::new())
7144            .unwrap();
7145        assert!(ids.is_empty());
7146        assert_eq!(store.count_for(&agent).unwrap(), 0);
7147    }
7148
7149    #[test]
7150    fn test_semantic_keys_matching_returns_substring_matches() {
7151        let store = SemanticStore::new();
7152        store.store("rust_intro", "value1", vec![]).unwrap();
7153        store.store("rust_advanced", "value2", vec![]).unwrap();
7154        store.store("python_basics", "value3", vec![]).unwrap();
7155        let matches = store.keys_matching("rust").unwrap();
7156        assert_eq!(matches.len(), 2);
7157        assert!(matches.contains(&"rust_intro".to_string()));
7158        assert!(matches.contains(&"rust_advanced".to_string()));
7159    }
7160
7161    #[test]
7162    fn test_semantic_keys_matching_is_case_insensitive() {
7163        let store = SemanticStore::new();
7164        store.store("UPPER_KEY", "v", vec![]).unwrap();
7165        let matches = store.keys_matching("upper").unwrap();
7166        assert_eq!(matches.len(), 1);
7167    }
7168
7169    #[test]
7170    fn test_semantic_keys_matching_empty_when_no_match() {
7171        let store = SemanticStore::new();
7172        store.store("abc", "val", vec![]).unwrap();
7173        let matches = store.keys_matching("xyz").unwrap();
7174        assert!(matches.is_empty());
7175    }
7176
7177    // ── Round 8: EpisodicStore::importance_avg / deduplicate_content ─────────
7178
7179    #[test]
7180    fn test_importance_avg_correct() {
7181        let store = EpisodicStore::new();
7182        let agent = AgentId::new("a");
7183        store.add_episode(agent.clone(), "e1", 0.2).unwrap();
7184        store.add_episode(agent.clone(), "e2", 0.8).unwrap();
7185        let avg = store.importance_avg(&agent).unwrap();
7186        assert!((avg - 0.5).abs() < 1e-5);
7187    }
7188
7189    #[test]
7190    fn test_importance_avg_zero_for_empty() {
7191        let store = EpisodicStore::new();
7192        let agent = AgentId::new("empty");
7193        assert_eq!(store.importance_avg(&agent).unwrap(), 0.0);
7194    }
7195
7196    #[test]
7197    fn test_deduplicate_content_removes_lower_importance_duplicate() {
7198        let store = EpisodicStore::new();
7199        let agent = AgentId::new("a");
7200        store.add_episode(agent.clone(), "same", 0.3).unwrap();
7201        store.add_episode(agent.clone(), "same", 0.9).unwrap();
7202        store.add_episode(agent.clone(), "different", 0.5).unwrap();
7203        let removed = store.deduplicate_content(&agent).unwrap();
7204        assert_eq!(removed, 1);
7205        assert_eq!(store.count_for(&agent).unwrap(), 2);
7206    }
7207
7208    #[test]
7209    fn test_deduplicate_content_keeps_highest_importance() {
7210        let store = EpisodicStore::new();
7211        let agent = AgentId::new("a");
7212        store.add_episode(agent.clone(), "dup", 0.1).unwrap();
7213        store.add_episode(agent.clone(), "dup", 0.7).unwrap();
7214        store.deduplicate_content(&agent).unwrap();
7215        let items = store.recall(&agent, 10).unwrap();
7216        assert_eq!(items.len(), 1);
7217        assert!((items[0].importance - 0.7).abs() < 1e-5);
7218    }
7219
7220    // ── Round 8: WorkingMemory::iter_sorted / SemanticStore::get_value ───────
7221
7222    #[test]
7223    fn test_working_memory_iter_sorted_alphabetical_order() {
7224        let wm = WorkingMemory::new(10).unwrap();
7225        wm.set("c", "3").unwrap();
7226        wm.set("a", "1").unwrap();
7227        wm.set("b", "2").unwrap();
7228        let sorted = wm.iter_sorted().unwrap();
7229        let keys: Vec<&str> = sorted.iter().map(|(k, _)| k.as_str()).collect();
7230        assert_eq!(keys, vec!["a", "b", "c"]);
7231    }
7232
7233    #[test]
7234    fn test_semantic_get_value_returns_value_for_existing_key() {
7235        let store = SemanticStore::new();
7236        store.store("mykey", "myvalue", vec![]).unwrap();
7237        assert_eq!(store.get_value("mykey").unwrap(), Some("myvalue".to_string()));
7238    }
7239
7240    #[test]
7241    fn test_semantic_get_value_returns_none_for_missing_key() {
7242        let store = SemanticStore::new();
7243        assert!(store.get_value("ghost").unwrap().is_none());
7244    }
7245
7246    // ── Round 9: recall_top_n / filter_by_importance / update_many / entry_count_with_embedding ──
7247
7248    #[test]
7249    fn test_recall_top_n_returns_highest_importance_items() {
7250        let store = EpisodicStore::new();
7251        let agent = AgentId::new("a");
7252        store.add_episode(agent.clone(), "low", 0.1).unwrap();
7253        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
7254        store.add_episode(agent.clone(), "high", 0.9).unwrap();
7255        let top = store.recall_top_n(&agent, 2).unwrap();
7256        assert_eq!(top.len(), 2);
7257        assert!((top[0].importance - 0.9).abs() < 1e-5);
7258    }
7259
7260    #[test]
7261    fn test_recall_top_n_clamps_to_available_items() {
7262        let store = EpisodicStore::new();
7263        let agent = AgentId::new("a");
7264        store.add_episode(agent.clone(), "only", 0.5).unwrap();
7265        let top = store.recall_top_n(&agent, 100).unwrap();
7266        assert_eq!(top.len(), 1);
7267    }
7268
7269    #[test]
7270    fn test_filter_by_importance_returns_items_in_range() {
7271        let store = EpisodicStore::new();
7272        let agent = AgentId::new("b");
7273        store.add_episode(agent.clone(), "low", 0.1).unwrap();
7274        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
7275        store.add_episode(agent.clone(), "high", 0.9).unwrap();
7276        let mid_range = store.filter_by_importance(&agent, 0.3, 0.7).unwrap();
7277        assert_eq!(mid_range.len(), 1);
7278        assert!((mid_range[0].importance - 0.5).abs() < 1e-5);
7279    }
7280
7281    #[test]
7282    fn test_update_many_sets_multiple_keys() {
7283        let wm = WorkingMemory::new(10).unwrap();
7284        wm.set("x", "old_x").unwrap();
7285        wm.set("y", "old_y").unwrap();
7286        let updated = wm
7287            .update_many(vec![("x".to_string(), "new_x".to_string()), ("y".to_string(), "new_y".to_string())])
7288            .unwrap();
7289        assert_eq!(updated, 2);
7290        assert_eq!(wm.get("x").unwrap(), Some("new_x".to_string()));
7291        assert_eq!(wm.get("y").unwrap(), Some("new_y".to_string()));
7292    }
7293
7294    #[test]
7295    fn test_update_many_returns_zero_for_empty_iter() {
7296        let wm = WorkingMemory::new(5).unwrap();
7297        let updated = wm.update_many(Vec::<(String, String)>::new()).unwrap();
7298        assert_eq!(updated, 0);
7299    }
7300
7301    #[test]
7302    fn test_entry_count_with_embedding_counts_only_embedded_entries() {
7303        let store = SemanticStore::new();
7304        store.store_with_embedding("has_emb", "v1", vec![], vec![0.1_f32, 0.2_f32]).unwrap();
7305        store.store("no_emb", "v2", vec![]).unwrap();
7306        assert_eq!(store.entry_count_with_embedding().unwrap(), 1);
7307    }
7308
7309    // ── Round 10: retain_top_n / keys_starting_with ───────────────────────────
7310
7311    #[test]
7312    fn test_retain_top_n_removes_low_importance_episodes() {
7313        let store = EpisodicStore::new();
7314        let agent = AgentId::new("a");
7315        store.add_episode(agent.clone(), "low", 0.1).unwrap();
7316        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
7317        store.add_episode(agent.clone(), "high", 0.9).unwrap();
7318        let removed = store.retain_top_n(&agent, 2).unwrap();
7319        assert_eq!(removed, 1);
7320        let remaining = store.recall(&agent, 10).unwrap();
7321        assert_eq!(remaining.len(), 2);
7322        assert!(remaining.iter().all(|i| i.importance >= 0.5));
7323    }
7324
7325    #[test]
7326    fn test_retain_top_n_noop_when_fewer_than_n() {
7327        let store = EpisodicStore::new();
7328        let agent = AgentId::new("b");
7329        store.add_episode(agent.clone(), "only", 0.7).unwrap();
7330        let removed = store.retain_top_n(&agent, 5).unwrap();
7331        assert_eq!(removed, 0);
7332        assert_eq!(store.recall(&agent, 10).unwrap().len(), 1);
7333    }
7334
7335    #[test]
7336    fn test_keys_starting_with_returns_matching_keys() {
7337        let wm = WorkingMemory::new(10).unwrap();
7338        wm.set("user:name", "alice").unwrap();
7339        wm.set("user:email", "alice@example.com").unwrap();
7340        wm.set("session:id", "abc").unwrap();
7341        let mut keys = wm.keys_starting_with("user:").unwrap();
7342        keys.sort();
7343        assert_eq!(keys, vec!["user:email", "user:name"]);
7344    }
7345
7346    #[test]
7347    fn test_keys_starting_with_returns_empty_when_no_match() {
7348        let wm = WorkingMemory::new(5).unwrap();
7349        wm.set("foo", "bar").unwrap();
7350        assert!(wm.keys_starting_with("xyz:").unwrap().is_empty());
7351    }
7352
7353    // ── Round 11: most_recent / contains_all / has_any_key / to_map / fill_ratio ──
7354
7355    #[test]
7356    fn test_episodic_most_recent_returns_last_inserted() {
7357        let store = EpisodicStore::new();
7358        let agent = AgentId::new("a");
7359        store.add_episode(agent.clone(), "first", 0.5).unwrap();
7360        store.add_episode(agent.clone(), "second", 0.3).unwrap();
7361        let recent = store.most_recent(&agent).unwrap().unwrap();
7362        assert_eq!(recent.content, "second");
7363    }
7364
7365    #[test]
7366    fn test_episodic_most_recent_none_when_empty() {
7367        let store = EpisodicStore::new();
7368        let agent = AgentId::new("nobody");
7369        assert!(store.most_recent(&agent).unwrap().is_none());
7370    }
7371
7372    #[test]
7373    fn test_working_memory_contains_all_true_when_all_present() {
7374        let wm = WorkingMemory::new(10).unwrap();
7375        wm.set("x", "1").unwrap();
7376        wm.set("y", "2").unwrap();
7377        assert!(wm.contains_all(["x", "y"]).unwrap());
7378    }
7379
7380    #[test]
7381    fn test_working_memory_contains_all_false_when_one_missing() {
7382        let wm = WorkingMemory::new(10).unwrap();
7383        wm.set("x", "1").unwrap();
7384        assert!(!wm.contains_all(["x", "missing"]).unwrap());
7385    }
7386
7387    #[test]
7388    fn test_working_memory_contains_all_vacuously_true_for_empty() {
7389        let wm = WorkingMemory::new(5).unwrap();
7390        assert!(wm.contains_all(std::iter::empty::<&str>()).unwrap());
7391    }
7392
7393    #[test]
7394    fn test_working_memory_has_any_key_true_when_at_least_one_present() {
7395        let wm = WorkingMemory::new(5).unwrap();
7396        wm.set("present", "v").unwrap();
7397        assert!(wm.has_any_key(["missing", "present"]).unwrap());
7398    }
7399
7400    #[test]
7401    fn test_working_memory_has_any_key_false_when_none_present() {
7402        let wm = WorkingMemory::new(5).unwrap();
7403        assert!(!wm.has_any_key(["a", "b"]).unwrap());
7404    }
7405
7406    #[test]
7407    fn test_working_memory_has_any_key_false_for_empty_iter() {
7408        let wm = WorkingMemory::new(5).unwrap();
7409        assert!(!wm.has_any_key(std::iter::empty::<&str>()).unwrap());
7410    }
7411
7412    #[test]
7413    fn test_semantic_to_map_returns_key_value_pairs() {
7414        let store = SemanticStore::new();
7415        store.store("key1", "val1", vec![]).unwrap();
7416        store.store("key2", "val2", vec![]).unwrap();
7417        let map = store.to_map().unwrap();
7418        assert_eq!(map.get("key1").map(String::as_str), Some("val1"));
7419        assert_eq!(map.get("key2").map(String::as_str), Some("val2"));
7420        assert_eq!(map.len(), 2);
7421    }
7422
7423    #[test]
7424    fn test_semantic_to_map_empty_when_no_entries() {
7425        let store = SemanticStore::new();
7426        assert!(store.to_map().unwrap().is_empty());
7427    }
7428
7429    #[test]
7430    fn test_working_memory_fill_ratio_zero_when_empty() {
7431        let wm = WorkingMemory::new(10).unwrap();
7432        assert_eq!(wm.fill_ratio().unwrap(), 0.0);
7433    }
7434
7435    #[test]
7436    fn test_working_memory_fill_ratio_correct_proportion() {
7437        let wm = WorkingMemory::new(4).unwrap();
7438        wm.set("a", "1").unwrap();
7439        wm.set("b", "2").unwrap();
7440        // 2 out of 4 = 0.5
7441        assert!((wm.fill_ratio().unwrap() - 0.5).abs() < 1e-9);
7442    }
7443
7444    // ── Round 11: most_recent / contains_all / has_any_key ───────────────────
7445
7446    #[test]
7447    fn test_most_recent_returns_last_inserted_episode() {
7448        let store = EpisodicStore::new();
7449        let agent = AgentId::new("a");
7450        store.add_episode(agent.clone(), "first", 0.5).unwrap();
7451        store.add_episode(agent.clone(), "second", 0.8).unwrap();
7452        let recent = store.most_recent(&agent).unwrap().unwrap();
7453        assert_eq!(recent.content, "second");
7454    }
7455
7456    #[test]
7457    fn test_most_recent_returns_none_for_new_agent() {
7458        let store = EpisodicStore::new();
7459        let agent = AgentId::new("empty");
7460        assert!(store.most_recent(&agent).unwrap().is_none());
7461    }
7462
7463    #[test]
7464    fn test_contains_all_true_when_all_keys_present() {
7465        let wm = WorkingMemory::new(10).unwrap();
7466        wm.set("a", "1").unwrap();
7467        wm.set("b", "2").unwrap();
7468        assert!(wm.contains_all(["a", "b"]).unwrap());
7469    }
7470
7471    #[test]
7472    fn test_contains_all_false_when_one_key_missing() {
7473        let wm = WorkingMemory::new(10).unwrap();
7474        wm.set("a", "1").unwrap();
7475        assert!(!wm.contains_all(["a", "missing"]).unwrap());
7476    }
7477
7478    #[test]
7479    fn test_contains_all_true_for_empty_iter() {
7480        let wm = WorkingMemory::new(5).unwrap();
7481        assert!(wm.contains_all([]).unwrap());
7482    }
7483
7484    #[test]
7485    fn test_has_any_key_true_when_one_key_present() {
7486        let wm = WorkingMemory::new(10).unwrap();
7487        wm.set("x", "v").unwrap();
7488        assert!(wm.has_any_key(["x", "y"]).unwrap());
7489    }
7490
7491    #[test]
7492    fn test_has_any_key_false_when_none_present() {
7493        let wm = WorkingMemory::new(5).unwrap();
7494        assert!(!wm.has_any_key(["nope", "also_nope"]).unwrap());
7495    }
7496
7497    #[test]
7498    fn test_has_any_key_false_for_empty_iter() {
7499        let wm = WorkingMemory::new(5).unwrap();
7500        wm.set("a", "1").unwrap();
7501        assert!(!wm.has_any_key([]).unwrap());
7502    }
7503
7504    // ── Round 13: EpisodicStore::most_recalled ────────────────────────────────
7505
7506    #[test]
7507    fn test_most_recalled_returns_none_for_new_agent() {
7508        let store = EpisodicStore::new();
7509        let agent = AgentId::new("fresh");
7510        assert!(store.most_recalled(&agent).unwrap().is_none());
7511    }
7512
7513    #[test]
7514    fn test_most_recalled_returns_highest_recall_count() {
7515        use std::sync::Arc;
7516        let store = EpisodicStore::new();
7517        let agent = AgentId::new("a");
7518        store.add_episode(agent.clone(), "low", 0.3).unwrap();
7519        store.add_episode(agent.clone(), "high", 0.9).unwrap();
7520        store.add_episode(agent.clone(), "mid", 0.6).unwrap();
7521        // Increment recall count on "high" episode by recalling it multiple times
7522        let items = store.recall(&agent, 3).unwrap();
7523        // recall_count is incremented on each recall; all items start at 0 after add,
7524        // then each recall increments them. We need to find which has highest recall_count.
7525        // After recall(3), all 3 items get incremented once. Let's recall just "high" more.
7526        // Simulate: call recall again to increment counts further
7527        store.recall(&agent, 1).unwrap();
7528        let top = store.most_recalled(&agent).unwrap();
7529        assert!(top.is_some());
7530        // The most recalled has recall_count >= 1
7531        assert!(top.unwrap().recall_count >= 1);
7532    }
7533
7534    #[test]
7535    fn test_most_recalled_single_episode() {
7536        let store = EpisodicStore::new();
7537        let agent = AgentId::new("solo");
7538        store.add_episode(agent.clone(), "only one", 0.7).unwrap();
7539        store.recall(&agent, 1).unwrap();
7540        let top = store.most_recalled(&agent).unwrap();
7541        assert_eq!(top.unwrap().content, "only one");
7542    }
7543
7544    // ── Round 13: max_importance / min_importance / values_matching ──────────
7545
7546    #[test]
7547    fn test_max_importance_returns_highest_score() {
7548        let store = EpisodicStore::new();
7549        let agent = AgentId::new("a");
7550        store.add_episode(agent.clone(), "low", 0.1).unwrap();
7551        store.add_episode(agent.clone(), "high", 0.9).unwrap();
7552        let max = store.max_importance(&agent).unwrap().unwrap();
7553        assert!((max - 0.9).abs() < 1e-5);
7554    }
7555
7556    #[test]
7557    fn test_min_importance_returns_lowest_score() {
7558        let store = EpisodicStore::new();
7559        let agent = AgentId::new("b");
7560        store.add_episode(agent.clone(), "low", 0.1).unwrap();
7561        store.add_episode(agent.clone(), "high", 0.9).unwrap();
7562        let min = store.min_importance(&agent).unwrap().unwrap();
7563        assert!((min - 0.1).abs() < 1e-5);
7564    }
7565
7566    #[test]
7567    fn test_max_importance_none_for_empty_agent() {
7568        let store = EpisodicStore::new();
7569        let agent = AgentId::new("empty");
7570        assert!(store.max_importance(&agent).unwrap().is_none());
7571    }
7572
7573    #[test]
7574    fn test_values_matching_returns_pairs_with_pattern() {
7575        let wm = WorkingMemory::new(10).unwrap();
7576        wm.set("name", "alice wonder").unwrap();
7577        wm.set("city", "wonderland").unwrap();
7578        wm.set("role", "engineer").unwrap();
7579        let mut matches = wm.values_matching("wonder").unwrap();
7580        matches.sort_by_key(|(k, _)| k.clone());
7581        assert_eq!(
7582            matches,
7583            vec![
7584                ("city".to_string(), "wonderland".to_string()),
7585                ("name".to_string(), "alice wonder".to_string()),
7586            ]
7587        );
7588    }
7589
7590    #[test]
7591    fn test_values_matching_returns_empty_when_no_match() {
7592        let wm = WorkingMemory::new(5).unwrap();
7593        wm.set("a", "foo").unwrap();
7594        assert!(wm.values_matching("xyz").unwrap().is_empty());
7595    }
7596
7597    // ── Round 14: count_above_importance, value_length, tags_for ─────────────
7598
7599    #[test]
7600    fn test_count_above_importance_correct_count() {
7601        let store = EpisodicStore::new();
7602        let agent = AgentId::new("agent");
7603        store.add_episode(agent.clone(), "low", 0.2).unwrap();
7604        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
7605        store.add_episode(agent.clone(), "high", 0.9).unwrap();
7606        assert_eq!(store.count_above_importance(&agent, 0.4).unwrap(), 2);
7607    }
7608
7609    #[test]
7610    fn test_count_above_importance_zero_for_empty_agent() {
7611        let store = EpisodicStore::new();
7612        let agent = AgentId::new("nobody");
7613        assert_eq!(store.count_above_importance(&agent, 0.0).unwrap(), 0);
7614    }
7615
7616    #[test]
7617    fn test_count_above_importance_threshold_is_exclusive() {
7618        let store = EpisodicStore::new();
7619        let agent = AgentId::new("ex");
7620        store.add_episode(agent.clone(), "exact", 0.5).unwrap();
7621        // threshold 0.5 — strictly greater than, so 0.5 should not count
7622        assert_eq!(store.count_above_importance(&agent, 0.5).unwrap(), 0);
7623    }
7624
7625    #[test]
7626    fn test_working_memory_value_length_some_for_existing_key() {
7627        let wm = WorkingMemory::new(10).unwrap();
7628        wm.set("greeting", "hello").unwrap();
7629        assert_eq!(wm.value_length("greeting").unwrap(), Some(5));
7630    }
7631
7632    #[test]
7633    fn test_working_memory_value_length_none_for_absent_key() {
7634        let wm = WorkingMemory::new(10).unwrap();
7635        assert!(wm.value_length("missing").unwrap().is_none());
7636    }
7637
7638    #[test]
7639    fn test_semantic_store_tags_for_returns_tags() {
7640        let store = SemanticStore::new();
7641        store
7642            .store("key1", "value1", vec!["alpha".to_string(), "beta".to_string()])
7643            .unwrap();
7644        let tags = store.tags_for("key1").unwrap().unwrap();
7645        assert_eq!(tags, vec!["alpha".to_string(), "beta".to_string()]);
7646    }
7647
7648    #[test]
7649    fn test_semantic_store_tags_for_none_for_missing_key() {
7650        let store = SemanticStore::new();
7651        assert!(store.tags_for("ghost").unwrap().is_none());
7652    }
7653
7654    #[test]
7655    fn test_semantic_store_tags_for_empty_tags() {
7656        let store = SemanticStore::new();
7657        store.store("k", "v", vec![]).unwrap();
7658        let tags = store.tags_for("k").unwrap().unwrap();
7659        assert!(tags.is_empty());
7660    }
7661
7662    // ── Round 27: peek_oldest, SemanticStore::values, SemanticStore::get_tags ─
7663
7664    #[test]
7665    fn test_peek_oldest_returns_oldest_without_removing() {
7666        let wm = WorkingMemory::new(10).unwrap();
7667        wm.set("first", "alpha").unwrap();
7668        wm.set("second", "beta").unwrap();
7669        let peeked = wm.peek_oldest().unwrap();
7670        assert_eq!(peeked, Some(("first".into(), "alpha".into())));
7671        // Still there after peek
7672        assert_eq!(wm.len().unwrap(), 2);
7673    }
7674
7675    #[test]
7676    fn test_peek_oldest_empty_returns_none() {
7677        let wm = WorkingMemory::new(5).unwrap();
7678        assert_eq!(wm.peek_oldest().unwrap(), None);
7679    }
7680
7681    #[test]
7682    fn test_peek_oldest_does_not_remove_entry() {
7683        let wm = WorkingMemory::new(5).unwrap();
7684        wm.set("k", "v").unwrap();
7685        wm.peek_oldest().unwrap();
7686        assert_eq!(wm.get("k").unwrap(), Some("v".into()));
7687    }
7688
7689    #[test]
7690    fn test_semantic_store_values_returns_all_values() {
7691        let store = SemanticStore::new();
7692        store.store("k1", "hello", vec![]).unwrap();
7693        store.store("k2", "world", vec![]).unwrap();
7694        let vals = store.values().unwrap();
7695        assert_eq!(vals.len(), 2);
7696        assert!(vals.contains(&"hello".to_string()));
7697        assert!(vals.contains(&"world".to_string()));
7698    }
7699
7700    #[test]
7701    fn test_semantic_store_values_empty() {
7702        let store = SemanticStore::new();
7703        assert!(store.values().unwrap().is_empty());
7704    }
7705
7706    #[test]
7707    fn test_semantic_store_get_tags_returns_tags() {
7708        let store = SemanticStore::new();
7709        store.store("k1", "val", vec!["tag-a".to_string(), "tag-b".to_string()]).unwrap();
7710        let tags = store.get_tags("k1").unwrap();
7711        assert!(tags.is_some());
7712        let tags = tags.unwrap();
7713        assert!(tags.contains(&"tag-a".to_string()));
7714        assert!(tags.contains(&"tag-b".to_string()));
7715    }
7716
7717    #[test]
7718    fn test_semantic_store_get_tags_missing_key_returns_none() {
7719        let store = SemanticStore::new();
7720        assert!(store.get_tags("no-such-key").unwrap().is_none());
7721    }
7722
7723    // ── Round 16 (duplicate block): WorkingMemory::value_length, iter_sorted ──
7724
7725    #[test]
7726    fn test_working_memory_value_length_returns_char_count_r27() {
7727        let wm = WorkingMemory::new(10).unwrap();
7728        wm.set("k", "hello").unwrap();
7729        assert_eq!(wm.value_length("k").unwrap(), Some(5));
7730    }
7731
7732    #[test]
7733    fn test_working_memory_value_length_none_for_missing_key_r27() {
7734        let wm = WorkingMemory::new(10).unwrap();
7735        assert!(wm.value_length("nope").unwrap().is_none());
7736    }
7737
7738    #[test]
7739    fn test_working_memory_iter_sorted_returns_sorted_pairs() {
7740        let wm = WorkingMemory::new(10).unwrap();
7741        wm.set("b", "2").unwrap();
7742        wm.set("a", "1").unwrap();
7743        let pairs = wm.iter_sorted().unwrap();
7744        assert_eq!(pairs[0].0, "a");
7745        assert_eq!(pairs[1].0, "b");
7746    }
7747
7748    #[test]
7749    fn test_working_memory_drain_empties_store() {
7750        let wm = WorkingMemory::new(10).unwrap();
7751        wm.set("x", "1").unwrap();
7752        wm.set("y", "2").unwrap();
7753        let drained = wm.drain().unwrap();
7754        assert_eq!(drained.len(), 2);
7755        assert!(wm.is_empty().unwrap());
7756    }
7757
7758    #[test]
7759    fn test_working_memory_snapshot_returns_all_entries_r27() {
7760        let wm = WorkingMemory::new(10).unwrap();
7761        wm.set("a", "alpha").unwrap();
7762        wm.set("b", "beta").unwrap();
7763        let snap = wm.snapshot().unwrap();
7764        assert_eq!(snap.get("a").map(String::as_str), Some("alpha"));
7765        assert_eq!(snap.get("b").map(String::as_str), Some("beta"));
7766    }
7767
7768    #[test]
7769    fn test_working_memory_snapshot_empty_when_no_entries_r27() {
7770        let wm = WorkingMemory::new(5).unwrap();
7771        assert!(wm.snapshot().unwrap().is_empty());
7772    }
7773
7774    // ── Round 15: agent_count, is_at_capacity, remove_keys_starting_with,
7775    //              has_key, entry_count_with_tag ─────────────────────────────
7776
7777    #[test]
7778    fn test_episodic_store_agent_count_zero_when_empty() {
7779        let store = EpisodicStore::new();
7780        assert_eq!(store.agent_count().unwrap(), 0);
7781    }
7782
7783    #[test]
7784    fn test_episodic_store_agent_count_distinct_agents() {
7785        let store = EpisodicStore::new();
7786        store.add_episode(AgentId::new("a"), "ep1", 0.5).unwrap();
7787        store.add_episode(AgentId::new("b"), "ep2", 0.7).unwrap();
7788        store.add_episode(AgentId::new("a"), "ep3", 0.3).unwrap();
7789        assert_eq!(store.agent_count().unwrap(), 2);
7790    }
7791
7792    #[test]
7793    fn test_working_memory_is_at_capacity_true_when_full() {
7794        let wm = WorkingMemory::new(2).unwrap();
7795        wm.set("k1", "v1").unwrap();
7796        wm.set("k2", "v2").unwrap();
7797        assert!(wm.is_at_capacity().unwrap());
7798    }
7799
7800    #[test]
7801    fn test_working_memory_is_at_capacity_false_when_not_full() {
7802        let wm = WorkingMemory::new(5).unwrap();
7803        wm.set("k1", "v1").unwrap();
7804        assert!(!wm.is_at_capacity().unwrap());
7805    }
7806
7807    #[test]
7808    fn test_remove_keys_starting_with_removes_matching_keys() {
7809        let wm = WorkingMemory::new(10).unwrap();
7810        wm.set("prefix_a", "1").unwrap();
7811        wm.set("prefix_b", "2").unwrap();
7812        wm.set("other", "3").unwrap();
7813        let removed = wm.remove_keys_starting_with("prefix_").unwrap();
7814        assert_eq!(removed, 2);
7815        assert!(wm.get("prefix_a").unwrap().is_none());
7816        assert!(wm.get("prefix_b").unwrap().is_none());
7817        assert_eq!(wm.get("other").unwrap(), Some("3".into()));
7818    }
7819
7820    #[test]
7821    fn test_remove_keys_starting_with_returns_zero_when_no_match() {
7822        let wm = WorkingMemory::new(5).unwrap();
7823        wm.set("foo", "bar").unwrap();
7824        assert_eq!(wm.remove_keys_starting_with("xyz_").unwrap(), 0);
7825    }
7826
7827    #[test]
7828    fn test_semantic_store_has_key_true_when_exists() {
7829        let store = SemanticStore::new();
7830        store.store("mykey", "val", vec![]).unwrap();
7831        assert!(store.has_key("mykey").unwrap());
7832    }
7833
7834    #[test]
7835    fn test_semantic_store_has_key_false_when_absent() {
7836        let store = SemanticStore::new();
7837        assert!(!store.has_key("ghost").unwrap());
7838    }
7839
7840    #[test]
7841    fn test_entry_count_with_tag_counts_correctly() {
7842        let store = SemanticStore::new();
7843        store.store("k1", "v1", vec!["rust".into(), "async".into()]).unwrap();
7844        store.store("k2", "v2", vec!["rust".into()]).unwrap();
7845        store.store("k3", "v3", vec!["python".into()]).unwrap();
7846        assert_eq!(store.entry_count_with_tag("rust").unwrap(), 2);
7847        assert_eq!(store.entry_count_with_tag("async").unwrap(), 1);
7848        assert_eq!(store.entry_count_with_tag("absent").unwrap(), 0);
7849    }
7850
7851    // ── Round 18: importance_sum, update_content, recall_all, top_n, search_by_importance_range ──
7852
7853    #[test]
7854    fn test_importance_sum_returns_sum_of_all_importances() {
7855        let store = EpisodicStore::new();
7856        let agent = AgentId::new("a");
7857        store.add_episode(agent.clone(), "e1", 0.2).unwrap();
7858        store.add_episode(agent.clone(), "e2", 0.3).unwrap();
7859        store.add_episode(agent.clone(), "e3", 0.5).unwrap();
7860        let sum = store.importance_sum(&agent).unwrap();
7861        assert!((sum - 1.0).abs() < 1e-5);
7862    }
7863
7864    #[test]
7865    fn test_importance_sum_zero_for_unknown_agent() {
7866        let store = EpisodicStore::new();
7867        let agent = AgentId::new("nobody");
7868        assert!((store.importance_sum(&agent).unwrap() - 0.0).abs() < 1e-9);
7869    }
7870
7871    #[test]
7872    fn test_update_content_changes_stored_content() {
7873        let store = EpisodicStore::new();
7874        let agent = AgentId::new("a");
7875        let id = store.add_episode(agent.clone(), "old content", 0.5).unwrap();
7876        let updated = store.update_content(&agent, &id, "new content").unwrap();
7877        assert!(updated);
7878        let items = store.recall_all(&agent).unwrap();
7879        assert_eq!(items[0].content, "new content");
7880    }
7881
7882    #[test]
7883    fn test_update_content_false_for_missing_id() {
7884        let store = EpisodicStore::new();
7885        let agent = AgentId::new("a");
7886        let fake_id = MemoryId::new("nonexistent");
7887        assert!(!store.update_content(&agent, &fake_id, "x").unwrap());
7888    }
7889
7890    #[test]
7891    fn test_recall_all_returns_all_episodes() {
7892        let store = EpisodicStore::new();
7893        let agent = AgentId::new("a");
7894        store.add_episode(agent.clone(), "e1", 0.5).unwrap();
7895        store.add_episode(agent.clone(), "e2", 0.3).unwrap();
7896        store.add_episode(agent.clone(), "e3", 0.9).unwrap();
7897        let all = store.recall_all(&agent).unwrap();
7898        assert_eq!(all.len(), 3);
7899    }
7900
7901    #[test]
7902    fn test_top_n_returns_top_by_importance() {
7903        let store = EpisodicStore::new();
7904        let agent = AgentId::new("a");
7905        store.add_episode(agent.clone(), "low", 0.1).unwrap();
7906        store.add_episode(agent.clone(), "high", 0.9).unwrap();
7907        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
7908        let top = store.top_n(&agent, 2).unwrap();
7909        assert_eq!(top.len(), 2);
7910        assert_eq!(top[0].content, "high");
7911        assert_eq!(top[1].content, "mid");
7912    }
7913
7914    #[test]
7915    fn test_search_by_importance_range_filters_correctly() {
7916        let store = EpisodicStore::new();
7917        let agent = AgentId::new("a");
7918        store.add_episode(agent.clone(), "low", 0.1).unwrap();
7919        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
7920        store.add_episode(agent.clone(), "high", 0.9).unwrap();
7921        let results = store.search_by_importance_range(&agent, 0.4, 0.8, 0).unwrap();
7922        assert_eq!(results.len(), 1);
7923        assert_eq!(results[0].content, "mid");
7924    }
7925
7926    // ── Round 16: oldest_episode, remove_by_key, total_value_bytes ───────────
7927
7928    #[test]
7929    fn test_oldest_episode_returns_none_for_new_agent() {
7930        let store = EpisodicStore::new();
7931        let agent = AgentId::new("fresh");
7932        assert!(store.oldest_episode(&agent).unwrap().is_none());
7933    }
7934
7935    #[test]
7936    fn test_oldest_episode_returns_earliest_timestamp() {
7937        let store = EpisodicStore::new();
7938        let agent = AgentId::new("a");
7939        store.add_episode(agent.clone(), "first", 0.5).unwrap();
7940        store.add_episode(agent.clone(), "second", 0.7).unwrap();
7941        store.add_episode(agent.clone(), "third", 0.3).unwrap();
7942        let oldest = store.oldest_episode(&agent).unwrap().unwrap();
7943        // Insertion order determines timestamp; "first" should be oldest
7944        assert_eq!(oldest.content, "first");
7945    }
7946
7947    #[test]
7948    fn test_semantic_store_remove_by_key_removes_all_matching() {
7949        let store = SemanticStore::new();
7950        store.store("target", "v1", vec![]).unwrap();
7951        store.store("target", "v2", vec![]).unwrap();
7952        store.store("keep", "vk", vec![]).unwrap();
7953        let removed = store.remove_by_key("target").unwrap();
7954        assert_eq!(removed, 2);
7955        assert!(!store.has_key("target").unwrap());
7956        assert!(store.has_key("keep").unwrap());
7957    }
7958
7959    #[test]
7960    fn test_semantic_store_remove_by_key_zero_for_absent_key() {
7961        let store = SemanticStore::new();
7962        assert_eq!(store.remove_by_key("ghost").unwrap(), 0);
7963    }
7964
7965    #[test]
7966    fn test_working_memory_total_value_bytes_sums_lengths() {
7967        let wm = WorkingMemory::new(10).unwrap();
7968        wm.set("a", "hello").unwrap();    // 5 bytes
7969        wm.set("b", "world!").unwrap();   // 6 bytes
7970        assert_eq!(wm.total_value_bytes().unwrap(), 11);
7971    }
7972
7973    #[test]
7974    fn test_working_memory_total_value_bytes_zero_when_empty() {
7975        let wm = WorkingMemory::new(5).unwrap();
7976        assert_eq!(wm.total_value_bytes().unwrap(), 0);
7977    }
7978
7979    // ── Round 19: agent_ids, clear_for, recall_since ──────────────────────────
7980
7981    #[test]
7982    fn test_agent_ids_returns_all_tracked_agents() {
7983        let store = EpisodicStore::new();
7984        let a1 = AgentId::new("alice");
7985        let a2 = AgentId::new("bob");
7986        store.add_episode(a1.clone(), "x", 0.5).unwrap();
7987        store.add_episode(a2.clone(), "y", 0.5).unwrap();
7988        let mut ids = store.agent_ids().unwrap();
7989        ids.sort_by_key(|id| id.as_str().to_string());
7990        assert_eq!(ids.len(), 2);
7991        assert_eq!(ids[0].as_str(), "alice");
7992        assert_eq!(ids[1].as_str(), "bob");
7993    }
7994
7995    #[test]
7996    fn test_agent_ids_empty_for_new_store() {
7997        let store = EpisodicStore::new();
7998        assert!(store.agent_ids().unwrap().is_empty());
7999    }
8000
8001    #[test]
8002    fn test_clear_for_removes_all_episodes_for_agent() {
8003        let store = EpisodicStore::new();
8004        let agent = AgentId::new("a");
8005        store.add_episode(agent.clone(), "e1", 0.5).unwrap();
8006        store.add_episode(agent.clone(), "e2", 0.3).unwrap();
8007        let removed = store.clear_for(&agent).unwrap();
8008        assert_eq!(removed, 2);
8009        assert_eq!(store.count_for(&agent).unwrap(), 0);
8010    }
8011
8012    #[test]
8013    fn test_clear_for_returns_zero_for_unknown_agent() {
8014        let store = EpisodicStore::new();
8015        let agent = AgentId::new("ghost");
8016        assert_eq!(store.clear_for(&agent).unwrap(), 0);
8017    }
8018
8019    #[test]
8020    fn test_recall_since_returns_episodes_after_cutoff() {
8021        let store = EpisodicStore::new();
8022        let agent = AgentId::new("a");
8023        let past = chrono::Utc::now() - chrono::Duration::hours(2);
8024        let future_cutoff = chrono::Utc::now() + chrono::Duration::seconds(1);
8025        // Add one in the past
8026        store.add_episode_at(agent.clone(), "old", 0.5, past).unwrap();
8027        // Add one now
8028        store.add_episode(agent.clone(), "new", 0.5).unwrap();
8029        // Recall only future episodes (should be 0)
8030        let future = store.recall_since(&agent, future_cutoff, 0).unwrap();
8031        assert!(future.is_empty());
8032        // Recall from far past (should include both)
8033        let all = store.recall_since(&agent, past - chrono::Duration::seconds(1), 0).unwrap();
8034        assert_eq!(all.len(), 2);
8035    }
8036
8037    // ── Round 17: EpisodicStore::sum_recall_counts ────────────────────────────
8038
8039    #[test]
8040    fn test_sum_recall_counts_zero_for_new_agent() {
8041        let store = EpisodicStore::new();
8042        let agent = AgentId::new("new");
8043        assert_eq!(store.sum_recall_counts(&agent).unwrap(), 0);
8044    }
8045
8046    #[test]
8047    fn test_sum_recall_counts_increases_with_recalls() {
8048        let store = EpisodicStore::new();
8049        let agent = AgentId::new("a");
8050        store.add_episode(agent.clone(), "ep1", 0.5).unwrap();
8051        store.add_episode(agent.clone(), "ep2", 0.8).unwrap();
8052        // Recall both episodes once
8053        store.recall(&agent, 2).unwrap();
8054        let total = store.sum_recall_counts(&agent).unwrap();
8055        assert!(total >= 2);
8056    }
8057
8058    // ── Round 22: max_recall_count_for, most_recent_key, max_key_length ───────
8059
8060    #[test]
8061    fn test_max_recall_count_for_none_for_new_agent() {
8062        let store = EpisodicStore::new();
8063        let agent = AgentId::new("ghost");
8064        assert_eq!(store.max_recall_count_for(&agent).unwrap(), None);
8065    }
8066
8067    #[test]
8068    fn test_max_recall_count_for_returns_highest_after_recalls() {
8069        let store = EpisodicStore::new();
8070        let agent = AgentId::new("a");
8071        store.add_episode(agent.clone(), "ep1", 0.9).unwrap();
8072        store.add_episode(agent.clone(), "ep2", 0.1).unwrap();
8073        // Recall several times to accumulate counts
8074        store.recall(&agent, 2).unwrap();
8075        store.recall(&agent, 1).unwrap();
8076        let max = store.max_recall_count_for(&agent).unwrap().unwrap();
8077        assert!(max >= 1);
8078    }
8079
8080    #[test]
8081    fn test_semantic_most_recent_key_none_when_empty() {
8082        let store = SemanticStore::new();
8083        assert_eq!(store.most_recent_key().unwrap(), None);
8084    }
8085
8086    #[test]
8087    fn test_semantic_most_recent_key_returns_last_inserted() {
8088        let store = SemanticStore::new();
8089        store.store("first", "v1", vec![]).unwrap();
8090        store.store("second", "v2", vec![]).unwrap();
8091        assert_eq!(store.most_recent_key().unwrap(), Some("second".to_string()));
8092    }
8093
8094    #[test]
8095    fn test_working_memory_max_key_length_zero_when_empty() {
8096        let mem = WorkingMemory::new(10).unwrap();
8097        assert_eq!(mem.max_key_length().unwrap(), 0);
8098    }
8099
8100    #[test]
8101    fn test_working_memory_max_key_length_returns_longest() {
8102        let mem = WorkingMemory::new(10).unwrap();
8103        mem.set("ab", "v1").unwrap();
8104        mem.set("abcde", "v2").unwrap();
8105        mem.set("abc", "v3").unwrap();
8106        assert_eq!(mem.max_key_length().unwrap(), 5);
8107    }
8108
8109    // ── Round 29: WorkingMemory::set_if_absent ────────────────────────────────
8110
8111    #[test]
8112    fn test_working_memory_set_if_absent_inserts_new_key() {
8113        let mem = WorkingMemory::new(10).unwrap();
8114        let inserted = mem.set_if_absent("fresh", "value").unwrap();
8115        assert!(inserted);
8116        assert_eq!(mem.get("fresh").unwrap(), Some("value".to_string()));
8117    }
8118
8119    #[test]
8120    fn test_working_memory_set_if_absent_does_not_overwrite_existing() {
8121        let mem = WorkingMemory::new(10).unwrap();
8122        mem.set("key", "original").unwrap();
8123        let inserted = mem.set_if_absent("key", "replacement").unwrap();
8124        assert!(!inserted);
8125        assert_eq!(mem.get("key").unwrap(), Some("original".to_string()));
8126    }
8127
8128    #[test]
8129    fn test_working_memory_set_if_absent_second_call_returns_false() {
8130        let mem = WorkingMemory::new(10).unwrap();
8131        assert!(mem.set_if_absent("k", "v1").unwrap());
8132        assert!(!mem.set_if_absent("k", "v2").unwrap());
8133    }
8134
8135    // ── Round 24: MemoryItem helpers, DecayPolicy, export/bump ───────────────
8136
8137    #[test]
8138    fn test_memory_item_has_tag_true_when_tag_present() {
8139        let item = MemoryItem::new(
8140            AgentId::new("a"),
8141            "content",
8142            0.5,
8143            vec!["important".to_string(), "work".to_string()],
8144        );
8145        assert!(item.has_tag("important"));
8146        assert!(item.has_tag("work"));
8147    }
8148
8149    #[test]
8150    fn test_memory_item_has_tag_false_when_tag_absent() {
8151        let item = MemoryItem::new(AgentId::new("a"), "content", 0.5, vec![]);
8152        assert!(!item.has_tag("missing"));
8153    }
8154
8155    #[test]
8156    fn test_memory_item_word_count_counts_words() {
8157        let item = MemoryItem::new(AgentId::new("a"), "one two three", 0.5, vec![]);
8158        assert_eq!(item.word_count(), 3);
8159    }
8160
8161    #[test]
8162    fn test_memory_item_word_count_zero_for_empty_content() {
8163        let item = MemoryItem::new(AgentId::new("a"), "", 0.5, vec![]);
8164        assert_eq!(item.word_count(), 0);
8165    }
8166
8167    #[test]
8168    fn test_decay_policy_apply_reduces_importance_after_one_half_life() {
8169        let policy = DecayPolicy::exponential(1.0).unwrap(); // half-life 1 hour
8170        let decayed = policy.apply(1.0, 1.0); // after 1 hour, should be ~0.5
8171        assert!((decayed - 0.5).abs() < 1e-5);
8172    }
8173
8174    #[test]
8175    fn test_decay_policy_apply_no_change_for_zero_age() {
8176        let policy = DecayPolicy::exponential(10.0).unwrap();
8177        let decayed = policy.apply(0.8, 0.0);
8178        assert!((decayed - 0.8).abs() < 1e-5);
8179    }
8180
8181    #[test]
8182    fn test_decay_policy_decay_item_reduces_importance_for_old_item() {
8183        let policy = DecayPolicy::exponential(0.0001).unwrap(); // very short half-life
8184        let mut item = MemoryItem::new(AgentId::new("a"), "old memory", 1.0, vec![]);
8185        item.timestamp = Utc::now() - chrono::Duration::hours(1);
8186        policy.decay_item(&mut item);
8187        assert!(item.importance < 1.0);
8188    }
8189
8190    #[test]
8191    fn test_episodic_export_returns_empty_for_unknown_agent() {
8192        let store = EpisodicStore::new();
8193        let agent = AgentId::new("ghost");
8194        let exported = store.export_agent_memory(&agent).unwrap();
8195        assert!(exported.is_empty());
8196    }
8197
8198    #[test]
8199    fn test_episodic_export_returns_all_stored_items() {
8200        let store = EpisodicStore::new();
8201        let agent = AgentId::new("a");
8202        store.add_episode(agent.clone(), "memory1", 0.9).unwrap();
8203        store.add_episode(agent.clone(), "memory2", 0.5).unwrap();
8204        let exported = store.export_agent_memory(&agent).unwrap();
8205        assert_eq!(exported.len(), 2);
8206    }
8207
8208    #[test]
8209    fn test_bump_recall_count_increases_by_amount() {
8210        let store = EpisodicStore::new();
8211        let agent = AgentId::new("a");
8212        store.add_episode(agent.clone(), "the content", 0.7).unwrap();
8213        store.bump_recall_count_by_content("the content", 5);
8214        let max = store.max_recall_count_for(&agent).unwrap().unwrap();
8215        assert_eq!(max, 5);
8216    }
8217
8218    #[test]
8219    fn test_bump_recall_count_no_effect_for_absent_content() {
8220        let store = EpisodicStore::new();
8221        let agent = AgentId::new("a");
8222        store.add_episode(agent.clone(), "existing", 0.5).unwrap();
8223        store.bump_recall_count_by_content("not here", 10);
8224        let max = store.max_recall_count_for(&agent).unwrap().unwrap();
8225        assert_eq!(max, 0);
8226    }
8227
8228    // ── Round 23: latest_episode / oldest_key / key_count_matching / avg_value_length
8229
8230    #[test]
8231    fn test_latest_episode_none_for_new_agent() {
8232        let store = EpisodicStore::new();
8233        let agent = AgentId::new("ghost");
8234        assert!(store.latest_episode(&agent).unwrap().is_none());
8235    }
8236
8237    #[test]
8238    fn test_latest_episode_returns_most_recent_by_timestamp() {
8239        let store = EpisodicStore::new();
8240        let agent = AgentId::new("a");
8241        let old = chrono::Utc::now() - chrono::Duration::hours(1);
8242        store.add_episode_at(agent.clone(), "old ep", 0.5, old).unwrap();
8243        store.add_episode(agent.clone(), "new ep", 0.8).unwrap();
8244        let latest = store.latest_episode(&agent).unwrap().unwrap();
8245        assert_eq!(latest.content, "new ep");
8246    }
8247
8248    #[test]
8249    fn test_semantic_oldest_key_none_when_empty() {
8250        let store = SemanticStore::new();
8251        assert!(store.oldest_key().unwrap().is_none());
8252    }
8253
8254    #[test]
8255    fn test_semantic_oldest_key_returns_first_inserted() {
8256        let store = SemanticStore::new();
8257        store.store("first", "value1", vec![]).unwrap();
8258        store.store("second", "value2", vec![]).unwrap();
8259        assert_eq!(store.oldest_key().unwrap().as_deref(), Some("first"));
8260    }
8261
8262    #[test]
8263    fn test_working_memory_key_count_matching_zero_when_empty() {
8264        let mem = WorkingMemory::new(10).unwrap();
8265        assert_eq!(mem.key_count_matching("foo").unwrap(), 0);
8266    }
8267
8268    #[test]
8269    fn test_working_memory_key_count_matching_counts_correctly() {
8270        let mem = WorkingMemory::new(10).unwrap();
8271        mem.set("foo_bar", "v1").unwrap();
8272        mem.set("foo_baz", "v2").unwrap();
8273        mem.set("other", "v3").unwrap();
8274        assert_eq!(mem.key_count_matching("foo").unwrap(), 2);
8275    }
8276
8277    #[test]
8278    fn test_working_memory_avg_value_length_zero_when_empty() {
8279        let mem = WorkingMemory::new(10).unwrap();
8280        assert!((mem.avg_value_length().unwrap() - 0.0).abs() < 1e-9);
8281    }
8282
8283    #[test]
8284    fn test_working_memory_avg_value_length_correct_mean() {
8285        let mem = WorkingMemory::new(10).unwrap();
8286        mem.set("k1", "ab").unwrap();    // 2 bytes
8287        mem.set("k2", "abcd").unwrap();  // 4 bytes
8288        // mean = 3.0
8289        assert!((mem.avg_value_length().unwrap() - 3.0).abs() < 1e-9);
8290    }
8291
8292    // ── Round 30: EpisodicStore::has_agent, export_agent_memory ──────────────
8293
8294    #[test]
8295    fn test_episodic_store_has_agent_false_when_empty() {
8296        let store = EpisodicStore::new();
8297        assert!(!store.has_agent(&AgentId::new("nobody")).unwrap());
8298    }
8299
8300    #[test]
8301    fn test_episodic_store_has_agent_true_after_episode() {
8302        let store = EpisodicStore::new();
8303        let agent = AgentId::new("agent-ha");
8304        store.add_episode(agent.clone(), "event", 0.5).unwrap();
8305        assert!(store.has_agent(&agent).unwrap());
8306    }
8307
8308    #[test]
8309    fn test_episodic_store_export_agent_memory_empty_for_unknown() {
8310        let store = EpisodicStore::new();
8311        let exported = store.export_agent_memory(&AgentId::new("ghost")).unwrap();
8312        assert!(exported.is_empty());
8313    }
8314
8315    #[test]
8316    fn test_episodic_store_export_agent_memory_returns_episodes() {
8317        let store = EpisodicStore::new();
8318        let agent = AgentId::new("agent-exp");
8319        store.add_episode(agent.clone(), "ep1", 0.9).unwrap();
8320        store.add_episode(agent.clone(), "ep2", 0.7).unwrap();
8321        let exported = store.export_agent_memory(&agent).unwrap();
8322        assert_eq!(exported.len(), 2);
8323    }
8324
8325    // ── Round 30: SemanticStore::store_with_embedding ────────────────────────
8326
8327    #[test]
8328    fn test_semantic_store_store_with_embedding_rejects_empty_vec() {
8329        let store = SemanticStore::new();
8330        let result = store.store_with_embedding("k", "v", vec![], vec![]);
8331        assert!(result.is_err());
8332    }
8333
8334    #[test]
8335    fn test_semantic_store_store_with_embedding_stores_entry() {
8336        let store = SemanticStore::new();
8337        let emb = vec![1.0f32, 0.0, 0.0];
8338        store.store_with_embedding("k1", "v1", vec![], emb).unwrap();
8339        assert_eq!(store.len().unwrap(), 1);
8340    }
8341
8342    #[test]
8343    fn test_semantic_store_store_with_embedding_rejects_dimension_mismatch() {
8344        let store = SemanticStore::new();
8345        store.store_with_embedding("k1", "v1", vec![], vec![1.0f32, 0.0]).unwrap();
8346        // Second call with different dimension should fail
8347        let result = store.store_with_embedding("k2", "v2", vec![], vec![1.0f32, 0.0, 0.0]);
8348        assert!(result.is_err());
8349    }
8350
8351    // ── Round 24: avg_importance / importance_range / entries_without_tags /
8352    //             avg_tag_count_per_entry / longest_key / longest_value ────────
8353
8354    #[test]
8355    fn test_avg_importance_zero_for_new_agent() {
8356        let store = EpisodicStore::new();
8357        let agent = AgentId::new("ghost");
8358        assert!((store.avg_importance(&agent).unwrap() - 0.0).abs() < 1e-9);
8359    }
8360
8361    #[test]
8362    fn test_avg_importance_correct_mean() {
8363        let store = EpisodicStore::new();
8364        let agent = AgentId::new("a");
8365        store.add_episode(agent.clone(), "ep1", 0.2).unwrap();
8366        store.add_episode(agent.clone(), "ep2", 0.8).unwrap();
8367        // mean = 0.5
8368        assert!((store.avg_importance(&agent).unwrap() - 0.5).abs() < 1e-6);
8369    }
8370
8371    #[test]
8372    fn test_importance_range_none_for_new_agent() {
8373        let store = EpisodicStore::new();
8374        let agent = AgentId::new("ghost");
8375        assert!(store.importance_range(&agent).unwrap().is_none());
8376    }
8377
8378    #[test]
8379    fn test_importance_range_returns_min_max() {
8380        let store = EpisodicStore::new();
8381        let agent = AgentId::new("a");
8382        store.add_episode(agent.clone(), "ep1", 0.1).unwrap();
8383        store.add_episode(agent.clone(), "ep2", 0.9).unwrap();
8384        let (min, max) = store.importance_range(&agent).unwrap().unwrap();
8385        assert!((min - 0.1_f32).abs() < 1e-6);
8386        assert!((max - 0.9_f32).abs() < 1e-6);
8387    }
8388
8389    #[test]
8390    fn test_semantic_entries_without_tags_all_untagged() {
8391        let store = SemanticStore::new();
8392        store.store("k1", "v1", vec![]).unwrap();
8393        store.store("k2", "v2", vec![]).unwrap();
8394        assert_eq!(store.entries_without_tags().unwrap(), 2);
8395    }
8396
8397    #[test]
8398    fn test_semantic_entries_without_tags_mixed() {
8399        let store = SemanticStore::new();
8400        store.store("k1", "v1", vec!["tag".to_string()]).unwrap();
8401        store.store("k2", "v2", vec![]).unwrap();
8402        assert_eq!(store.entries_without_tags().unwrap(), 1);
8403    }
8404
8405    #[test]
8406    fn test_semantic_avg_tag_count_zero_when_empty() {
8407        let store = SemanticStore::new();
8408        assert!((store.avg_tag_count_per_entry().unwrap() - 0.0).abs() < 1e-9);
8409    }
8410
8411    #[test]
8412    fn test_semantic_avg_tag_count_correct_mean() {
8413        let store = SemanticStore::new();
8414        store.store("k1", "v1", vec!["a".to_string(), "b".to_string()]).unwrap(); // 2
8415        store.store("k2", "v2", vec![]).unwrap(); // 0
8416        // mean = 1.0
8417        assert!((store.avg_tag_count_per_entry().unwrap() - 1.0).abs() < 1e-9);
8418    }
8419
8420    #[test]
8421    fn test_working_memory_longest_key_none_when_empty() {
8422        let mem = WorkingMemory::new(10).unwrap();
8423        assert!(mem.longest_key().unwrap().is_none());
8424    }
8425
8426    #[test]
8427    fn test_working_memory_longest_key_returns_longest() {
8428        let mem = WorkingMemory::new(10).unwrap();
8429        mem.set("ab", "v1").unwrap();
8430        mem.set("abcde", "v2").unwrap();
8431        assert_eq!(mem.longest_key().unwrap().as_deref(), Some("abcde"));
8432    }
8433
8434    #[test]
8435    fn test_working_memory_longest_value_none_when_empty() {
8436        let mem = WorkingMemory::new(10).unwrap();
8437        assert!(mem.longest_value().unwrap().is_none());
8438    }
8439
8440    #[test]
8441    fn test_working_memory_longest_value_returns_longest() {
8442        let mem = WorkingMemory::new(10).unwrap();
8443        mem.set("k1", "short").unwrap();
8444        mem.set("k2", "much longer value").unwrap();
8445        assert_eq!(mem.longest_value().unwrap().as_deref(), Some("much longer value"));
8446    }
8447
8448    // ── Round 26: SemanticStore::store_with_embedding ─────────────────────────
8449
8450    #[test]
8451    fn test_semantic_store_with_embedding_rejects_empty_vector() {
8452        let store = SemanticStore::new();
8453        let result = store.store_with_embedding("k", "v", vec![], vec![]);
8454        assert!(result.is_err());
8455    }
8456
8457    #[test]
8458    fn test_semantic_store_with_embedding_stores_and_retrievable() {
8459        let store = SemanticStore::new();
8460        store.store_with_embedding("k", "v", vec![], vec![1.0, 0.0]).unwrap();
8461        let entry = store.retrieve_by_key("k").unwrap();
8462        assert_eq!(entry.map(|(val, _)| val), Some("v".to_string()));
8463    }
8464
8465    #[test]
8466    fn test_semantic_store_with_embedding_dimension_mismatch_errors() {
8467        let store = SemanticStore::new();
8468        store.store_with_embedding("k1", "v1", vec![], vec![1.0, 0.0]).unwrap();
8469        let result = store.store_with_embedding("k2", "v2", vec![], vec![1.0, 0.0, 0.0]);
8470        assert!(result.is_err());
8471    }
8472
8473    // ── Round 26: has_episodes / value_for / count_above_value_length ─────────
8474
8475    #[test]
8476    fn test_episodic_store_has_episodes_false_when_empty() {
8477        let store = EpisodicStore::new();
8478        let id = AgentId::new("agent-x");
8479        assert!(!store.has_episodes(&id).unwrap());
8480    }
8481
8482    #[test]
8483    fn test_episodic_store_has_episodes_true_after_recording() {
8484        let store = EpisodicStore::new();
8485        let id = AgentId::new("agent-y");
8486        store.add_episode(id.clone(), "e1", 0.8).unwrap();
8487        assert!(store.has_episodes(&id).unwrap());
8488    }
8489
8490    #[test]
8491    fn test_semantic_store_value_for_none_when_missing() {
8492        let store = SemanticStore::new();
8493        assert!(store.value_for("missing-key").unwrap().is_none());
8494    }
8495
8496    #[test]
8497    fn test_semantic_store_value_for_returns_stored_value() {
8498        let store = SemanticStore::new();
8499        store.store("mykey", "myvalue", vec![]).unwrap();
8500        assert_eq!(store.value_for("mykey").unwrap(), Some("myvalue".to_string()));
8501    }
8502
8503    #[test]
8504    fn test_working_memory_count_above_value_length_zero_when_empty() {
8505        let wm = WorkingMemory::new(10).unwrap();
8506        assert_eq!(wm.count_above_value_length(5).unwrap(), 0);
8507    }
8508
8509    #[test]
8510    fn test_working_memory_count_above_value_length_counts_correctly() {
8511        let wm = WorkingMemory::new(10).unwrap();
8512        wm.set("k1", "short").unwrap();        // 5 bytes
8513        wm.set("k2", "a longer value").unwrap(); // 13 bytes
8514        wm.set("k3", "hi").unwrap();             // 2 bytes
8515        // min_bytes=5: values strictly > 5 bytes: "a longer value" (13)
8516        assert_eq!(wm.count_above_value_length(5).unwrap(), 1);
8517    }
8518
8519    // ── Round 27: total_episode_count ─────────────────────────────────────────
8520
8521    #[test]
8522    fn test_total_episode_count_zero_when_empty() {
8523        let store = EpisodicStore::new();
8524        assert_eq!(store.total_episode_count().unwrap(), 0);
8525    }
8526
8527    #[test]
8528    fn test_total_episode_count_sums_across_agents() {
8529        let store = EpisodicStore::new();
8530        let a1 = AgentId::new("a1");
8531        let a2 = AgentId::new("a2");
8532        store.add_episode(a1.clone(), "e1", 0.5).unwrap();
8533        store.add_episode(a1.clone(), "e2", 0.6).unwrap();
8534        store.add_episode(a2.clone(), "e3", 0.7).unwrap();
8535        assert_eq!(store.total_episode_count().unwrap(), 3);
8536    }
8537
8538    // ── Round 28: agents_with_min_episodes / entries_with_no_tags / longest_value_key
8539
8540    #[test]
8541    fn test_agents_with_min_episodes_empty_when_below_min() {
8542        let store = EpisodicStore::new();
8543        let a = AgentId::new("a1");
8544        store.add_episode(a.clone(), "e1", 0.5).unwrap();
8545        // min=2 → no agents qualify
8546        assert!(store.agents_with_min_episodes(2).unwrap().is_empty());
8547    }
8548
8549    #[test]
8550    fn test_agents_with_min_episodes_includes_qualifying_agents() {
8551        let store = EpisodicStore::new();
8552        let a1 = AgentId::new("a1");
8553        let a2 = AgentId::new("a2");
8554        store.add_episode(a1.clone(), "e1", 0.5).unwrap();
8555        store.add_episode(a1.clone(), "e2", 0.6).unwrap();
8556        store.add_episode(a2.clone(), "only-one", 0.7).unwrap();
8557        let result = store.agents_with_min_episodes(2).unwrap();
8558        assert_eq!(result, vec![a1]);
8559    }
8560
8561    #[test]
8562    fn test_entries_with_no_tags_returns_empty_list_when_all_have_tags() {
8563        let store = SemanticStore::new();
8564        store.store("k1", "v1", vec!["tag".to_string()]).unwrap();
8565        assert!(store.entries_with_no_tags().unwrap().is_empty());
8566    }
8567
8568    #[test]
8569    fn test_entries_with_no_tags_returns_untagged_keys() {
8570        let store = SemanticStore::new();
8571        store.store("k1", "v1", vec![]).unwrap();
8572        store.store("k2", "v2", vec!["tag".to_string()]).unwrap();
8573        let result = store.entries_with_no_tags().unwrap();
8574        assert_eq!(result, vec!["k1".to_string()]);
8575    }
8576
8577    #[test]
8578    fn test_working_memory_longest_value_key_none_when_empty() {
8579        let wm = WorkingMemory::new(10).unwrap();
8580        assert!(wm.longest_value_key().unwrap().is_none());
8581    }
8582
8583    #[test]
8584    fn test_working_memory_longest_value_key_returns_key_with_longest_value() {
8585        let wm = WorkingMemory::new(10).unwrap();
8586        wm.set("short_key", "hi").unwrap();
8587        wm.set("long_key", "a much longer value string").unwrap();
8588        assert_eq!(wm.longest_value_key().unwrap(), Some("long_key".to_string()));
8589    }
8590
8591    // ── Round 29: agent_with_most_episodes / most_tagged_key / value_lengths ──
8592
8593    #[test]
8594    fn test_agent_with_most_episodes_none_when_empty() {
8595        let store = EpisodicStore::new();
8596        assert!(store.agent_with_most_episodes().unwrap().is_none());
8597    }
8598
8599    #[test]
8600    fn test_agent_with_most_episodes_returns_agent_with_most() {
8601        let store = EpisodicStore::new();
8602        let a1 = AgentId::new("a1");
8603        let a2 = AgentId::new("a2");
8604        store.add_episode(a1.clone(), "e1", 0.5).unwrap();
8605        store.add_episode(a2.clone(), "e1", 0.5).unwrap();
8606        store.add_episode(a2.clone(), "e2", 0.6).unwrap();
8607        assert_eq!(store.agent_with_most_episodes().unwrap(), Some(a2));
8608    }
8609
8610    #[test]
8611    fn test_most_tagged_key_none_when_empty() {
8612        let store = SemanticStore::new();
8613        assert!(store.most_tagged_key().unwrap().is_none());
8614    }
8615
8616    #[test]
8617    fn test_most_tagged_key_returns_key_with_most_tags() {
8618        let store = SemanticStore::new();
8619        store.store("k1", "v1", vec!["a".to_string()]).unwrap();
8620        store.store("k2", "v2", vec!["a".to_string(), "b".to_string(), "c".to_string()]).unwrap();
8621        store.store("k3", "v3", vec![]).unwrap();
8622        assert_eq!(store.most_tagged_key().unwrap(), Some("k2".to_string()));
8623    }
8624
8625    #[test]
8626    fn test_value_lengths_empty_when_empty() {
8627        let wm = WorkingMemory::new(10).unwrap();
8628        assert!(wm.value_lengths().unwrap().is_empty());
8629    }
8630
8631    #[test]
8632    fn test_value_lengths_returns_all_pairs() {
8633        let wm = WorkingMemory::new(10).unwrap();
8634        wm.set("k", "hello").unwrap();
8635        let lengths = wm.value_lengths().unwrap();
8636        assert_eq!(lengths.len(), 1);
8637        assert_eq!(lengths[0], ("k".to_string(), 5));
8638    }
8639
8640    // ── Round 30: importance_variance_for / count_matching_value / keys_with_value_longer_than
8641
8642    #[test]
8643    fn test_importance_variance_for_zero_when_fewer_than_two() {
8644        let store = EpisodicStore::new();
8645        let id = AgentId::new("a");
8646        store.add_episode(id.clone(), "e", 0.5).unwrap();
8647        assert!((store.importance_variance_for(&id).unwrap() - 0.0).abs() < 1e-6);
8648    }
8649
8650    #[test]
8651    fn test_importance_variance_for_nonzero_with_spread() {
8652        let store = EpisodicStore::new();
8653        let id = AgentId::new("a");
8654        store.add_episode(id.clone(), "e1", 0.0).unwrap();
8655        store.add_episode(id.clone(), "e2", 1.0).unwrap();
8656        // mean=0.5, variance = 0.25
8657        let v = store.importance_variance_for(&id).unwrap();
8658        assert!((v - 0.25).abs() < 1e-5);
8659    }
8660
8661    #[test]
8662    fn test_count_matching_value_zero_when_no_match() {
8663        let store = SemanticStore::new();
8664        store.store("k", "hello world", vec![]).unwrap();
8665        assert_eq!(store.count_matching_value("xyz").unwrap(), 0);
8666    }
8667
8668    #[test]
8669    fn test_count_matching_value_counts_containing_entries() {
8670        let store = SemanticStore::new();
8671        store.store("k1", "hello world", vec![]).unwrap();
8672        store.store("k2", "world peace", vec![]).unwrap();
8673        store.store("k3", "goodbye", vec![]).unwrap();
8674        assert_eq!(store.count_matching_value("world").unwrap(), 2);
8675    }
8676
8677    #[test]
8678    fn test_keys_with_value_longer_than_empty_when_all_short() {
8679        let wm = WorkingMemory::new(10).unwrap();
8680        wm.set("k", "hi").unwrap();
8681        assert!(wm.keys_with_value_longer_than(10).unwrap().is_empty());
8682    }
8683
8684    #[test]
8685    fn test_keys_with_value_longer_than_returns_qualifying_keys() {
8686        let wm = WorkingMemory::new(10).unwrap();
8687        wm.set("short", "hi").unwrap();
8688        wm.set("long", "this is a longer value").unwrap();
8689        let keys = wm.keys_with_value_longer_than(5).unwrap();
8690        assert_eq!(keys, vec!["long".to_string()]);
8691    }
8692
8693    #[test]
8694    fn test_episodic_store_max_importance_overall_returns_highest() {
8695        let store = EpisodicStore::new();
8696        let agent = AgentId::new("a");
8697        store.add_episode(agent.clone(), "low", 0.2).unwrap();
8698        store.add_episode(agent.clone(), "high", 0.9).unwrap();
8699        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
8700        let max = store.max_importance_overall().unwrap();
8701        assert!((max.unwrap() - 0.9).abs() < 1e-6);
8702    }
8703
8704    #[test]
8705    fn test_episodic_store_max_importance_overall_empty_returns_none() {
8706        let store = EpisodicStore::new();
8707        assert!(store.max_importance_overall().unwrap().is_none());
8708    }
8709
8710    #[test]
8711    fn test_semantic_store_rename_tag_updates_all_occurrences() {
8712        let store = SemanticStore::new();
8713        store.store("k1", "v1", vec!["old".to_string(), "x".to_string()]).unwrap();
8714        store.store("k2", "v2", vec!["old".to_string()]).unwrap();
8715        let count = store.rename_tag("old", "new").unwrap();
8716        assert_eq!(count, 2);
8717    }
8718
8719    #[test]
8720    fn test_semantic_store_rename_tag_nonexistent_returns_zero() {
8721        let store = SemanticStore::new();
8722        store.store("k", "v", vec!["alpha".to_string()]).unwrap();
8723        assert_eq!(store.rename_tag("missing", "new").unwrap(), 0);
8724    }
8725
8726    #[test]
8727    fn test_working_memory_entry_count_reflects_stored_entries() {
8728        let wm = WorkingMemory::new(10).unwrap();
8729        assert_eq!(wm.entry_count().unwrap(), 0);
8730        wm.set("a", "1").unwrap();
8731        wm.set("b", "2").unwrap();
8732        assert_eq!(wm.entry_count().unwrap(), 2);
8733    }
8734
8735    #[test]
8736    fn test_episode_count_for_returns_correct_count() {
8737        let store = EpisodicStore::new();
8738        let agent = AgentId::new("a");
8739        store.add_episode(agent.clone(), "e1", 0.5).unwrap();
8740        store.add_episode(agent.clone(), "e2", 0.5).unwrap();
8741        assert_eq!(store.episode_count_for(&agent).unwrap(), 2);
8742    }
8743
8744    #[test]
8745    fn test_episode_count_for_unknown_agent_returns_zero() {
8746        let store = EpisodicStore::new();
8747        assert_eq!(store.episode_count_for(&AgentId::new("x")).unwrap(), 0);
8748    }
8749
8750    #[test]
8751    fn test_semantic_store_unique_tags_returns_sorted_distinct_tags() {
8752        let store = SemanticStore::new();
8753        store.store("k1", "v1", vec!["b".to_string(), "a".to_string()]).unwrap();
8754        store.store("k2", "v2", vec!["a".to_string(), "c".to_string()]).unwrap();
8755        assert_eq!(store.unique_tags().unwrap(), vec!["a", "b", "c"]);
8756    }
8757
8758    #[test]
8759    fn test_semantic_store_unique_tags_empty_returns_empty() {
8760        let store = SemanticStore::new();
8761        assert!(store.unique_tags().unwrap().is_empty());
8762    }
8763
8764    #[test]
8765    fn test_working_memory_count_matching_prefix_counts_correctly() {
8766        let wm = WorkingMemory::new(10).unwrap();
8767        wm.set("user:a", "1").unwrap();
8768        wm.set("user:b", "2").unwrap();
8769        wm.set("other", "3").unwrap();
8770        assert_eq!(wm.count_matching_prefix("user:").unwrap(), 2);
8771        assert_eq!(wm.count_matching_prefix("other").unwrap(), 1);
8772        assert_eq!(wm.count_matching_prefix("none").unwrap(), 0);
8773    }
8774
8775    #[test]
8776    fn test_episodic_store_total_content_bytes_sums_lengths() {
8777        let store = EpisodicStore::new();
8778        let agent = AgentId::new("a");
8779        store.add_episode(agent.clone(), "hi", 0.5).unwrap();   // 2 bytes
8780        store.add_episode(agent.clone(), "hello", 0.5).unwrap(); // 5 bytes
8781        assert_eq!(store.total_content_bytes(&agent).unwrap(), 7);
8782    }
8783
8784    #[test]
8785    fn test_episodic_store_total_content_bytes_unknown_agent_returns_zero() {
8786        let store = EpisodicStore::new();
8787        assert_eq!(store.total_content_bytes(&AgentId::new("x")).unwrap(), 0);
8788    }
8789
8790    #[test]
8791    fn test_episodic_store_avg_content_length_correct_mean() {
8792        let store = EpisodicStore::new();
8793        let agent = AgentId::new("a");
8794        store.add_episode(agent.clone(), "hi", 0.5).unwrap();    // 2
8795        store.add_episode(agent.clone(), "hello", 0.5).unwrap(); // 5
8796        assert!((store.avg_content_length(&agent).unwrap() - 3.5).abs() < 1e-9);
8797    }
8798
8799    #[test]
8800    fn test_episodic_store_avg_content_length_empty_returns_zero() {
8801        let store = EpisodicStore::new();
8802        assert_eq!(store.avg_content_length(&AgentId::new("x")).unwrap(), 0.0);
8803    }
8804
8805    #[test]
8806    fn test_semantic_store_keys_for_tag_returns_matching_keys() {
8807        let store = SemanticStore::new();
8808        store.store("k1", "v1", vec!["rust".to_string()]).unwrap();
8809        store.store("k2", "v2", vec!["python".to_string()]).unwrap();
8810        store.store("k3", "v3", vec!["rust".to_string(), "async".to_string()]).unwrap();
8811        let mut keys = store.keys_for_tag("rust").unwrap();
8812        keys.sort_unstable();
8813        assert_eq!(keys, vec!["k1", "k3"]);
8814    }
8815
8816    #[test]
8817    fn test_semantic_store_keys_for_tag_nonexistent_tag_returns_empty() {
8818        let store = SemanticStore::new();
8819        store.store("k", "v", vec!["rust".to_string()]).unwrap();
8820        assert!(store.keys_for_tag("missing").unwrap().is_empty());
8821    }
8822
8823    #[test]
8824    fn test_count_episodes_with_tag_returns_correct_count() {
8825        let store = EpisodicStore::new();
8826        let agent = AgentId::new("a");
8827        store.add_episode_with_tags(agent.clone(), "e1", 0.5, vec!["ai".to_string()]).unwrap();
8828        store.add_episode_with_tags(agent.clone(), "e2", 0.5, vec!["ai".to_string(), "ml".to_string()]).unwrap();
8829        store.add_episode_with_tags(agent.clone(), "e3", 0.5, vec!["ml".to_string()]).unwrap();
8830        assert_eq!(store.count_episodes_with_tag(&agent, "ai").unwrap(), 2);
8831        assert_eq!(store.count_episodes_with_tag(&agent, "ml").unwrap(), 2);
8832    }
8833
8834    #[test]
8835    fn test_episodes_with_content_returns_matching_content() {
8836        let store = EpisodicStore::new();
8837        let agent = AgentId::new("a");
8838        store.add_episode(agent.clone(), "rust is great", 0.5).unwrap();
8839        store.add_episode(agent.clone(), "python is fun", 0.5).unwrap();
8840        store.add_episode(agent.clone(), "rust and python", 0.5).unwrap();
8841        let matches = store.episodes_with_content(&agent, "rust").unwrap();
8842        assert_eq!(matches.len(), 2);
8843    }
8844
8845    #[test]
8846    fn test_semantic_store_most_common_tag_returns_most_frequent() {
8847        let store = SemanticStore::new();
8848        store.store("k1", "v1", vec!["a".to_string(), "b".to_string()]).unwrap();
8849        store.store("k2", "v2", vec!["a".to_string()]).unwrap();
8850        store.store("k3", "v3", vec!["b".to_string()]).unwrap();
8851        // "a" appears 2 times, "b" appears 2 times - either is valid
8852        let tag = store.most_common_tag().unwrap();
8853        assert!(tag.is_some());
8854    }
8855
8856    #[test]
8857    fn test_semantic_store_most_common_tag_empty_returns_none() {
8858        let store = SemanticStore::new();
8859        assert!(store.most_common_tag().unwrap().is_none());
8860    }
8861
8862    #[test]
8863    fn test_working_memory_pairs_starting_with_returns_matching_pairs() {
8864        let wm = WorkingMemory::new(10).unwrap();
8865        wm.set("user:name", "alice").unwrap();
8866        wm.set("user:age", "30").unwrap();
8867        wm.set("sys:mode", "prod").unwrap();
8868        let pairs = wm.pairs_starting_with("user:").unwrap();
8869        assert_eq!(pairs.len(), 2);
8870        assert!(pairs.iter().all(|(k, _)| k.starts_with("user:")));
8871    }
8872
8873    #[test]
8874    fn test_working_memory_total_key_bytes_sums_key_lengths() {
8875        let wm = WorkingMemory::new(10).unwrap();
8876        wm.set("ab", "x").unwrap();   // 2 bytes
8877        wm.set("cde", "y").unwrap();  // 3 bytes
8878        assert_eq!(wm.total_key_bytes().unwrap(), 5);
8879    }
8880
8881    #[test]
8882    fn test_working_memory_min_key_length_returns_shortest() {
8883        let wm = WorkingMemory::new(10).unwrap();
8884        wm.set("ab", "x").unwrap();
8885        wm.set("abcd", "y").unwrap();
8886        assert_eq!(wm.min_key_length().unwrap(), 2);
8887    }
8888
8889    #[test]
8890    fn test_working_memory_min_key_length_empty_returns_zero() {
8891        let wm = WorkingMemory::new(10).unwrap();
8892        assert_eq!(wm.min_key_length().unwrap(), 0);
8893    }
8894
8895    #[test]
8896    fn test_episodic_store_content_lengths_returns_lengths_in_order() {
8897        let store = EpisodicStore::new();
8898        let agent = AgentId::new("a");
8899        store.add_episode(agent.clone(), "hi", 0.5).unwrap();    // 2
8900        store.add_episode(agent.clone(), "hello", 0.5).unwrap(); // 5
8901        assert_eq!(store.content_lengths(&agent).unwrap(), vec![2, 5]);
8902    }
8903
8904    #[test]
8905    fn test_semantic_store_remove_entries_with_tag_removes_matching() {
8906        let store = SemanticStore::new();
8907        store.store("k1", "v1", vec!["old".to_string()]).unwrap();
8908        store.store("k2", "v2", vec!["keep".to_string()]).unwrap();
8909        store.store("k3", "v3", vec!["old".to_string(), "keep".to_string()]).unwrap();
8910        let removed = store.remove_entries_with_tag("old").unwrap();
8911        assert_eq!(removed, 2);
8912        assert_eq!(store.len().unwrap(), 1);
8913    }
8914
8915    // ── Round 36 ──────────────────────────────────────────────────────────────
8916
8917    #[test]
8918    fn test_episodic_store_max_content_length_returns_longest() {
8919        let store = EpisodicStore::new();
8920        let agent = AgentId::new("a");
8921        store.add_episode(agent.clone(), "hi", 0.5).unwrap();       // 2
8922        store.add_episode(agent.clone(), "hello!", 0.5).unwrap();   // 6
8923        store.add_episode(agent.clone(), "yo", 0.5).unwrap();       // 2
8924        assert_eq!(store.max_content_length(&agent).unwrap(), 6);
8925    }
8926
8927    #[test]
8928    fn test_episodic_store_max_content_length_unknown_agent_returns_zero() {
8929        let store = EpisodicStore::new();
8930        assert_eq!(store.max_content_length(&AgentId::new("x")).unwrap(), 0);
8931    }
8932
8933    #[test]
8934    fn test_episodic_store_min_content_length_returns_shortest() {
8935        let store = EpisodicStore::new();
8936        let agent = AgentId::new("a");
8937        store.add_episode(agent.clone(), "hi", 0.5).unwrap();      // 2
8938        store.add_episode(agent.clone(), "hello!", 0.5).unwrap();  // 6
8939        assert_eq!(store.min_content_length(&agent).unwrap(), 2);
8940    }
8941
8942    #[test]
8943    fn test_episodic_store_min_content_length_unknown_agent_returns_zero() {
8944        let store = EpisodicStore::new();
8945        assert_eq!(store.min_content_length(&AgentId::new("x")).unwrap(), 0);
8946    }
8947
8948    #[test]
8949    fn test_semantic_store_total_value_bytes_sums_value_lengths() {
8950        let store = SemanticStore::new();
8951        store.store("k1", "ab", vec![]).unwrap();    // 2
8952        store.store("k2", "cde", vec![]).unwrap();   // 3
8953        store.store("k3", "fghij", vec![]).unwrap(); // 5
8954        assert_eq!(store.total_value_bytes().unwrap(), 10);
8955    }
8956
8957    #[test]
8958    fn test_semantic_store_total_value_bytes_empty_returns_zero() {
8959        let store = SemanticStore::new();
8960        assert_eq!(store.total_value_bytes().unwrap(), 0);
8961    }
8962
8963    // ── Round 37 ──────────────────────────────────────────────────────────────
8964
8965    #[test]
8966    fn test_episodic_store_agents_with_episodes_returns_sorted_ids() {
8967        let store = EpisodicStore::new();
8968        let b = AgentId::new("b");
8969        let a = AgentId::new("a");
8970        store.add_episode(b.clone(), "episode b", 0.5).unwrap();
8971        store.add_episode(a.clone(), "episode a", 0.5).unwrap();
8972        let agents = store.agents_with_episodes().unwrap();
8973        assert_eq!(agents, vec![a, b]);
8974    }
8975
8976    #[test]
8977    fn test_episodic_store_agents_with_episodes_empty_returns_empty() {
8978        let store = EpisodicStore::new();
8979        assert!(store.agents_with_episodes().unwrap().is_empty());
8980    }
8981
8982    #[test]
8983    fn test_episodic_store_high_importance_count_counts_above_threshold() {
8984        let store = EpisodicStore::new();
8985        let agent = AgentId::new("a");
8986        store.add_episode(agent.clone(), "low", 0.3).unwrap();
8987        store.add_episode(agent.clone(), "high", 0.8).unwrap();
8988        store.add_episode(agent.clone(), "med", 0.6).unwrap();
8989        assert_eq!(store.high_importance_count(&agent, 0.5).unwrap(), 2);
8990    }
8991
8992    #[test]
8993    fn test_episodic_store_high_importance_count_unknown_agent_returns_zero() {
8994        let store = EpisodicStore::new();
8995        assert_eq!(store.high_importance_count(&AgentId::new("x"), 0.5).unwrap(), 0);
8996    }
8997
8998    #[test]
8999    fn test_semantic_store_avg_value_bytes_returns_mean() {
9000        let store = SemanticStore::new();
9001        store.store("k1", "ab", vec![]).unwrap();   // 2
9002        store.store("k2", "abcd", vec![]).unwrap(); // 4
9003        let avg = store.avg_value_bytes().unwrap();
9004        assert!((avg - 3.0).abs() < 1e-9);
9005    }
9006
9007    #[test]
9008    fn test_semantic_store_avg_value_bytes_empty_returns_zero() {
9009        let store = SemanticStore::new();
9010        assert_eq!(store.avg_value_bytes().unwrap(), 0.0);
9011    }
9012
9013    #[test]
9014    fn test_semantic_store_max_value_bytes_returns_longest() {
9015        let store = SemanticStore::new();
9016        store.store("k1", "hi", vec![]).unwrap();      // 2
9017        store.store("k2", "hello!", vec![]).unwrap();  // 6
9018        assert_eq!(store.max_value_bytes().unwrap(), 6);
9019    }
9020
9021    #[test]
9022    fn test_semantic_store_max_value_bytes_empty_returns_zero() {
9023        let store = SemanticStore::new();
9024        assert_eq!(store.max_value_bytes().unwrap(), 0);
9025    }
9026
9027    // ── Round 38 ──────────────────────────────────────────────────────────────
9028
9029    #[test]
9030    fn test_episodic_store_content_contains_count_counts_matches() {
9031        let store = EpisodicStore::new();
9032        let agent = AgentId::new("a");
9033        store.add_episode(agent.clone(), "rust is great", 0.5).unwrap();
9034        store.add_episode(agent.clone(), "python is ok", 0.5).unwrap();
9035        store.add_episode(agent.clone(), "rust rocks", 0.5).unwrap();
9036        assert_eq!(store.content_contains_count(&agent, "rust").unwrap(), 2);
9037        assert_eq!(store.content_contains_count(&agent, "java").unwrap(), 0);
9038    }
9039
9040    #[test]
9041    fn test_episodic_store_content_contains_count_unknown_agent_returns_zero() {
9042        let store = EpisodicStore::new();
9043        assert_eq!(store.content_contains_count(&AgentId::new("x"), "anything").unwrap(), 0);
9044    }
9045
9046    #[test]
9047    fn test_semantic_store_min_value_bytes_returns_shortest() {
9048        let store = SemanticStore::new();
9049        store.store("k1", "hello world", vec![]).unwrap(); // 11
9050        store.store("k2", "hi", vec![]).unwrap();          // 2
9051        assert_eq!(store.min_value_bytes().unwrap(), 2);
9052    }
9053
9054    #[test]
9055    fn test_semantic_store_min_value_bytes_empty_returns_zero() {
9056        let store = SemanticStore::new();
9057        assert_eq!(store.min_value_bytes().unwrap(), 0);
9058    }
9059
9060    #[test]
9061    fn test_working_memory_max_value_length_returns_longest() {
9062        let wm = WorkingMemory::new(10).unwrap();
9063        wm.set("k1", "ab").unwrap();     // 2
9064        wm.set("k2", "abcde").unwrap(); // 5
9065        assert_eq!(wm.max_value_length().unwrap(), 5);
9066    }
9067
9068    #[test]
9069    fn test_working_memory_max_value_length_empty_returns_zero() {
9070        let wm = WorkingMemory::new(10).unwrap();
9071        assert_eq!(wm.max_value_length().unwrap(), 0);
9072    }
9073
9074    // ── Round 39 ──────────────────────────────────────────────────────────────
9075
9076    #[test]
9077    fn test_episodic_store_episodes_by_importance_returns_desc_order() {
9078        let store = EpisodicStore::new();
9079        let agent = AgentId::new("a");
9080        store.add_episode(agent.clone(), "low", 0.2).unwrap();
9081        store.add_episode(agent.clone(), "high", 0.9).unwrap();
9082        store.add_episode(agent.clone(), "med", 0.5).unwrap();
9083        let contents = store.episodes_by_importance(&agent).unwrap();
9084        assert_eq!(contents[0], "high");
9085        assert_eq!(contents[2], "low");
9086    }
9087
9088    #[test]
9089    fn test_episodic_store_episodes_by_importance_empty_agent_returns_empty() {
9090        let store = EpisodicStore::new();
9091        assert!(store.episodes_by_importance(&AgentId::new("x")).unwrap().is_empty());
9092    }
9093
9094    #[test]
9095    fn test_semantic_store_all_keys_returns_sorted_keys() {
9096        let store = SemanticStore::new();
9097        store.store("banana", "v1", vec![]).unwrap();
9098        store.store("apple", "v2", vec![]).unwrap();
9099        store.store("cherry", "v3", vec![]).unwrap();
9100        assert_eq!(store.all_keys().unwrap(), vec!["apple", "banana", "cherry"]);
9101    }
9102
9103    #[test]
9104    fn test_semantic_store_all_keys_empty_returns_empty() {
9105        let store = SemanticStore::new();
9106        assert!(store.all_keys().unwrap().is_empty());
9107    }
9108
9109    #[test]
9110    fn test_working_memory_min_value_length_returns_shortest() {
9111        let wm = WorkingMemory::new(10).unwrap();
9112        wm.set("k1", "ab").unwrap();    // 2
9113        wm.set("k2", "abcde").unwrap(); // 5
9114        assert_eq!(wm.min_value_length().unwrap(), 2);
9115    }
9116
9117    #[test]
9118    fn test_working_memory_min_value_length_empty_returns_zero() {
9119        let wm = WorkingMemory::new(10).unwrap();
9120        assert_eq!(wm.min_value_length().unwrap(), 0);
9121    }
9122
9123    // ── Round 40 ──────────────────────────────────────────────────────────────
9124
9125    #[test]
9126    fn test_episodic_store_episode_count_above_importance_counts_correctly() {
9127        let store = EpisodicStore::new();
9128        let agent = AgentId::new("a");
9129        store.add_episode(agent.clone(), "low", 0.2).unwrap();
9130        store.add_episode(agent.clone(), "high", 0.9).unwrap();
9131        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
9132        assert_eq!(store.episode_count_above_importance(&agent, 0.4).unwrap(), 2);
9133        assert_eq!(store.episode_count_above_importance(&agent, 0.8).unwrap(), 1);
9134    }
9135
9136    #[test]
9137    fn test_episodic_store_episode_count_above_importance_unknown_agent_returns_zero() {
9138        let store = EpisodicStore::new();
9139        assert_eq!(store.episode_count_above_importance(&AgentId::new("x"), 0.5).unwrap(), 0);
9140    }
9141
9142    #[test]
9143    fn test_episodic_store_low_importance_episodes_sorted_ascending() {
9144        let store = EpisodicStore::new();
9145        let agent = AgentId::new("a");
9146        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
9147        store.add_episode(agent.clone(), "low", 0.1).unwrap();
9148        store.add_episode(agent.clone(), "high", 0.9).unwrap();
9149        let items = store.low_importance_episodes(&agent, 0.6).unwrap();
9150        assert_eq!(items.len(), 2);
9151        assert!(items[0].importance <= items[1].importance);
9152    }
9153
9154    #[test]
9155    fn test_episodic_store_low_importance_episodes_empty_when_none_below_threshold() {
9156        let store = EpisodicStore::new();
9157        let agent = AgentId::new("a");
9158        store.add_episode(agent.clone(), "high", 0.9).unwrap();
9159        assert!(store.low_importance_episodes(&agent, 0.5).unwrap().is_empty());
9160    }
9161
9162    #[test]
9163    fn test_working_memory_values_containing_returns_matching_values() {
9164        let wm = WorkingMemory::new(10).unwrap();
9165        wm.set("k1", "hello world").unwrap();
9166        wm.set("k2", "goodbye world").unwrap();
9167        wm.set("k3", "something else").unwrap();
9168        let result = wm.values_containing("world").unwrap();
9169        assert_eq!(result.len(), 2);
9170        assert!(result.iter().all(|v| v.contains("world")));
9171    }
9172
9173    #[test]
9174    fn test_working_memory_values_containing_returns_empty_when_no_match() {
9175        let wm = WorkingMemory::new(10).unwrap();
9176        wm.set("k1", "hello").unwrap();
9177        assert!(wm.values_containing("xyz").unwrap().is_empty());
9178    }
9179
9180    // ── Round 40 (continued): SemanticStore::keys_with_prefix ─────────────────
9181
9182    #[test]
9183    fn test_semantic_store_keys_with_prefix_returns_matching_keys_sorted() {
9184        let store = SemanticStore::new();
9185        store.store("user:alice", "data-a", vec![]).unwrap();
9186        store.store("user:bob", "data-b", vec![]).unwrap();
9187        store.store("session:123", "sess", vec![]).unwrap();
9188        let keys = store.keys_with_prefix("user:").unwrap();
9189        assert_eq!(keys, vec!["user:alice", "user:bob"]);
9190    }
9191
9192    #[test]
9193    fn test_semantic_store_keys_with_prefix_returns_empty_when_no_match() {
9194        let store = SemanticStore::new();
9195        store.store("abc", "v", vec![]).unwrap();
9196        assert!(store.keys_with_prefix("xyz").unwrap().is_empty());
9197    }
9198
9199    #[test]
9200    fn test_semantic_store_keys_with_prefix_empty_prefix_returns_all_keys() {
9201        let store = SemanticStore::new();
9202        store.store("b", "v2", vec![]).unwrap();
9203        store.store("a", "v1", vec![]).unwrap();
9204        let keys = store.keys_with_prefix("").unwrap();
9205        assert_eq!(keys, vec!["a", "b"]);
9206    }
9207
9208    // ── Round 41 ──────────────────────────────────────────────────────────────
9209
9210    #[test]
9211    fn test_episodic_store_episodes_sorted_by_timestamp_oldest_first() {
9212        use std::thread::sleep;
9213        use std::time::Duration;
9214        let store = EpisodicStore::new();
9215        let agent = AgentId::new("a");
9216        store.add_episode(agent.clone(), "first", 0.5).unwrap();
9217        sleep(Duration::from_millis(2));
9218        store.add_episode(agent.clone(), "second", 0.5).unwrap();
9219        let items = store.episodes_sorted_by_timestamp(&agent).unwrap();
9220        assert_eq!(items.len(), 2);
9221        assert!(items[0].timestamp <= items[1].timestamp);
9222    }
9223
9224    #[test]
9225    fn test_episodic_store_episodes_sorted_by_timestamp_unknown_agent_empty() {
9226        let store = EpisodicStore::new();
9227        assert!(store.episodes_sorted_by_timestamp(&AgentId::new("unknown")).unwrap().is_empty());
9228    }
9229
9230    #[test]
9231    fn test_working_memory_keys_sorted_returns_alphabetical_order() {
9232        let wm = WorkingMemory::new(10).unwrap();
9233        wm.set("banana", "b").unwrap();
9234        wm.set("apple", "a").unwrap();
9235        wm.set("cherry", "c").unwrap();
9236        assert_eq!(wm.keys_sorted().unwrap(), vec!["apple", "banana", "cherry"]);
9237    }
9238
9239    #[test]
9240    fn test_working_memory_keys_sorted_empty_returns_empty() {
9241        let wm = WorkingMemory::new(10).unwrap();
9242        assert!(wm.keys_sorted().unwrap().is_empty());
9243    }
9244
9245    // ── Round 41: MemoryItem tag management + predicates ─────────────────────
9246
9247    #[test]
9248    fn test_memory_item_content_len_returns_byte_length() {
9249        let item = MemoryItem::new(AgentId::new("a"), "hello", 0.5, vec![]);
9250        assert_eq!(item.content_len(), 5);
9251    }
9252
9253    #[test]
9254    fn test_memory_item_tag_count_returns_count() {
9255        let item = MemoryItem::new(AgentId::new("a"), "x", 0.5, vec!["t1".into(), "t2".into()]);
9256        assert_eq!(item.tag_count(), 2);
9257    }
9258
9259    #[test]
9260    fn test_memory_item_tag_count_zero_when_no_tags() {
9261        let item = MemoryItem::new(AgentId::new("a"), "x", 0.5, vec![]);
9262        assert_eq!(item.tag_count(), 0);
9263    }
9264
9265    #[test]
9266    fn test_memory_item_add_tag_adds_new_tag() {
9267        let mut item = MemoryItem::new(AgentId::new("a"), "x", 0.5, vec![]);
9268        assert!(item.add_tag("rust"));
9269        assert!(item.has_tag("rust"));
9270        assert_eq!(item.tag_count(), 1);
9271    }
9272
9273    #[test]
9274    fn test_memory_item_add_tag_returns_false_for_duplicate() {
9275        let mut item = MemoryItem::new(AgentId::new("a"), "x", 0.5, vec!["rust".into()]);
9276        assert!(!item.add_tag("rust"));
9277        assert_eq!(item.tag_count(), 1);
9278    }
9279
9280    #[test]
9281    fn test_memory_item_remove_tag_removes_existing_tag() {
9282        let mut item = MemoryItem::new(AgentId::new("a"), "x", 0.5, vec!["rust".into()]);
9283        assert!(item.remove_tag("rust"));
9284        assert!(!item.has_tag("rust"));
9285        assert_eq!(item.tag_count(), 0);
9286    }
9287
9288    #[test]
9289    fn test_memory_item_remove_tag_returns_false_when_not_present() {
9290        let mut item = MemoryItem::new(AgentId::new("a"), "x", 0.5, vec![]);
9291        assert!(!item.remove_tag("missing"));
9292    }
9293
9294    #[test]
9295    fn test_memory_item_is_high_importance_true_above_threshold() {
9296        let item = MemoryItem::new(AgentId::new("a"), "x", 0.9, vec![]);
9297        assert!(item.is_high_importance(0.7));
9298    }
9299
9300    #[test]
9301    fn test_memory_item_is_high_importance_false_at_threshold() {
9302        // strictly greater than — equal to threshold is NOT high importance
9303        let item = MemoryItem::new(AgentId::new("a"), "x", 0.7, vec![]);
9304        assert!(!item.is_high_importance(0.7));
9305    }
9306
9307    #[test]
9308    fn test_memory_item_is_high_importance_false_below_threshold() {
9309        let item = MemoryItem::new(AgentId::new("a"), "x", 0.3, vec![]);
9310        assert!(!item.is_high_importance(0.5));
9311    }
9312
9313    // ── Round 41: EpisodicStore::recall_count_for ──────────────────────────────
9314
9315    #[test]
9316    fn test_episodic_store_recall_count_for_returns_zero_initially() {
9317        let store = EpisodicStore::new();
9318        let agent = AgentId::new("a");
9319        let id = store.add_episode(agent.clone(), "content", 0.5).unwrap();
9320        let count = store.recall_count_for(&agent, &id).unwrap();
9321        assert_eq!(count, Some(0));
9322    }
9323
9324    #[test]
9325    fn test_episodic_store_recall_count_for_increments_after_recall() {
9326        let store = EpisodicStore::new();
9327        let agent = AgentId::new("a");
9328        let id = store.add_episode(agent.clone(), "content", 0.5).unwrap();
9329        store.recall(&agent, 10).unwrap();
9330        let count = store.recall_count_for(&agent, &id).unwrap();
9331        assert_eq!(count, Some(1));
9332    }
9333
9334    #[test]
9335    fn test_episodic_store_recall_count_for_returns_none_for_unknown_id() {
9336        let store = EpisodicStore::new();
9337        let agent = AgentId::new("a");
9338        let missing = MemoryId::new("no-such-id");
9339        assert_eq!(store.recall_count_for(&agent, &missing).unwrap(), None);
9340    }
9341
9342    // ── Round 42 ──────────────────────────────────────────────────────────────
9343
9344    #[test]
9345    fn test_working_memory_value_for_longest_key_returns_correct_value() {
9346        let wm = WorkingMemory::new(10).unwrap();
9347        wm.set("short", "v1").unwrap();
9348        wm.set("this-is-longer", "v2").unwrap();
9349        assert_eq!(wm.value_for_longest_key().unwrap(), Some("v2".to_string()));
9350    }
9351
9352    #[test]
9353    fn test_working_memory_value_for_longest_key_returns_none_when_empty() {
9354        let wm = WorkingMemory::new(10).unwrap();
9355        assert_eq!(wm.value_for_longest_key().unwrap(), None);
9356    }
9357
9358    // ── Round 42: DecayPolicy Display ─────────────────────────────────────────
9359
9360    #[test]
9361    fn test_decay_policy_display_format() {
9362        let p = DecayPolicy::exponential(24.0).unwrap();
9363        assert_eq!(p.to_string(), "Exponential(half_life=24.0h)");
9364    }
9365
9366    #[test]
9367    fn test_decay_policy_display_fractional_hours() {
9368        let p = DecayPolicy::exponential(1.5).unwrap();
9369        assert_eq!(p.to_string(), "Exponential(half_life=1.5h)");
9370    }
9371
9372    // ── Round 43 ──────────────────────────────────────────────────────────────
9373
9374    #[test]
9375    fn test_episodic_store_all_unique_tags_returns_sorted_deduped() {
9376        let store = EpisodicStore::new();
9377        let agent = AgentId::new("a");
9378        store.add_episode_with_tags(
9379            agent.clone(), "c1", 0.5, vec!["rust".into(), "ai".into()]
9380        ).unwrap();
9381        store.add_episode_with_tags(
9382            agent.clone(), "c2", 0.5, vec!["ai".into(), "ml".into()]
9383        ).unwrap();
9384        let tags = store.all_unique_tags(&agent).unwrap();
9385        assert_eq!(tags, vec!["ai", "ml", "rust"]);
9386    }
9387
9388    #[test]
9389    fn test_episodic_store_all_unique_tags_empty_for_unknown_agent() {
9390        let store = EpisodicStore::new();
9391        assert!(store.all_unique_tags(&AgentId::new("unknown")).unwrap().is_empty());
9392    }
9393
9394    #[test]
9395    fn test_episodic_store_episodes_with_tag_returns_matching() {
9396        let store = EpisodicStore::new();
9397        let agent = AgentId::new("a");
9398        store.add_episode_with_tags(
9399            agent.clone(), "tagged", 0.5, vec!["rust".into()]
9400        ).unwrap();
9401        store.add_episode(agent.clone(), "no-tag", 0.5).unwrap();
9402        let result = store.episodes_with_tag(&agent, "rust").unwrap();
9403        assert_eq!(result.len(), 1);
9404        assert_eq!(result[0].content, "tagged");
9405    }
9406
9407    #[test]
9408    fn test_episodic_store_episodes_with_tag_empty_when_no_match() {
9409        let store = EpisodicStore::new();
9410        let agent = AgentId::new("a");
9411        store.add_episode(agent.clone(), "no-tag", 0.5).unwrap();
9412        assert!(store.episodes_with_tag(&agent, "missing-tag").unwrap().is_empty());
9413    }
9414
9415    #[test]
9416    fn test_working_memory_contains_value_true_when_present() {
9417        let wm = WorkingMemory::new(10).unwrap();
9418        wm.set("k", "hello").unwrap();
9419        assert!(wm.contains_value("hello").unwrap());
9420    }
9421
9422    #[test]
9423    fn test_working_memory_contains_value_false_when_absent() {
9424        let wm = WorkingMemory::new(10).unwrap();
9425        assert!(!wm.contains_value("anything").unwrap());
9426    }
9427
9428    // ── Round 42: shortest_key, count_below_value_length, most_recent_episode ──
9429
9430    #[test]
9431    fn test_working_memory_shortest_key_returns_shortest() {
9432        let wm = WorkingMemory::new(10).unwrap();
9433        wm.set("abc", "v").unwrap();
9434        wm.set("a", "v").unwrap();
9435        wm.set("ab", "v").unwrap();
9436        assert_eq!(wm.shortest_key().unwrap(), Some("a".to_string()));
9437    }
9438
9439    #[test]
9440    fn test_working_memory_shortest_key_returns_none_when_empty() {
9441        let wm = WorkingMemory::new(10).unwrap();
9442        assert_eq!(wm.shortest_key().unwrap(), None);
9443    }
9444
9445    #[test]
9446    fn test_working_memory_count_below_value_length_counts_correctly() {
9447        let wm = WorkingMemory::new(10).unwrap();
9448        wm.set("k1", "hi").unwrap();      // 2 bytes
9449        wm.set("k2", "hello").unwrap();   // 5 bytes
9450        wm.set("k3", "yo").unwrap();      // 2 bytes
9451        // strictly less than 5: "hi" and "yo"
9452        assert_eq!(wm.count_below_value_length(5).unwrap(), 2);
9453    }
9454
9455    #[test]
9456    fn test_working_memory_count_below_value_length_zero_for_empty_store() {
9457        let wm = WorkingMemory::new(10).unwrap();
9458        assert_eq!(wm.count_below_value_length(100).unwrap(), 0);
9459    }
9460
9461    #[test]
9462    fn test_episodic_store_most_recent_episode_returns_latest() {
9463        use std::thread::sleep;
9464        use std::time::Duration;
9465        let store = EpisodicStore::new();
9466        let agent = AgentId::new("a");
9467        store.add_episode(agent.clone(), "old", 0.5).unwrap();
9468        sleep(Duration::from_millis(5));
9469        store.add_episode(agent.clone(), "new", 0.5).unwrap();
9470        let ep = store.most_recent_episode(&agent).unwrap().unwrap();
9471        assert_eq!(ep.content, "new");
9472    }
9473
9474    #[test]
9475    fn test_episodic_store_most_recent_episode_returns_none_for_unknown_agent() {
9476        let store = EpisodicStore::new();
9477        assert!(store.most_recent_episode(&AgentId::new("nobody")).unwrap().is_none());
9478    }
9479
9480    // ── Round 43: most_recalled_episode, value_for_shortest_key ───────────────
9481
9482    #[test]
9483    fn test_episodic_store_most_recalled_episode_returns_highest_recall_count() {
9484        let store = EpisodicStore::new();
9485        let agent = AgentId::new("a");
9486        store.add_episode(agent.clone(), "low", 0.5).unwrap();
9487        let id_high = store.add_episode(agent.clone(), "high", 0.8).unwrap();
9488        // recall the second episode twice
9489        store.recall(&agent, 10).unwrap();
9490        store.recall(&agent, 10).unwrap();
9491        let ep = store.most_recalled_episode(&agent).unwrap().unwrap();
9492        assert_eq!(ep.id, id_high);
9493    }
9494
9495    #[test]
9496    fn test_episodic_store_most_recalled_episode_returns_none_for_unknown_agent() {
9497        let store = EpisodicStore::new();
9498        assert!(store.most_recalled_episode(&AgentId::new("nobody")).unwrap().is_none());
9499    }
9500
9501    #[test]
9502    fn test_working_memory_value_for_shortest_key_returns_correct_value() {
9503        let wm = WorkingMemory::new(10).unwrap();
9504        wm.set("abc", "long-key").unwrap();
9505        wm.set("a", "short-key").unwrap();
9506        wm.set("ab", "mid-key").unwrap();
9507        assert_eq!(wm.value_for_shortest_key().unwrap(), Some("short-key".to_string()));
9508    }
9509
9510    #[test]
9511    fn test_working_memory_value_for_shortest_key_returns_none_when_empty() {
9512        let wm = WorkingMemory::new(10).unwrap();
9513        assert_eq!(wm.value_for_shortest_key().unwrap(), None);
9514    }
9515
9516    // ── Round 44: episode_count_before, entry_byte_pairs, key_with_longest_value
9517
9518    #[test]
9519    fn test_episodic_store_episode_count_before_counts_older_episodes() {
9520        use chrono::{Duration, Utc};
9521        let store = EpisodicStore::new();
9522        let agent = AgentId::new("a");
9523        store.add_episode(agent.clone(), "old", 0.5).unwrap();
9524        store.add_episode(agent.clone(), "newer", 0.5).unwrap();
9525        // All episodes have timestamps <= now, so count before far future == 2
9526        let future = Utc::now() + Duration::hours(1);
9527        assert_eq!(store.episode_count_before(&agent, future).unwrap(), 2);
9528        // Count before epoch == 0
9529        let past = Utc::now() - Duration::hours(1);
9530        assert_eq!(store.episode_count_before(&agent, past).unwrap(), 0);
9531    }
9532
9533    #[test]
9534    fn test_episodic_store_episode_count_before_zero_for_unknown_agent() {
9535        use chrono::Utc;
9536        let store = EpisodicStore::new();
9537        let future = Utc::now();
9538        assert_eq!(store.episode_count_before(&AgentId::new("nobody"), future).unwrap(), 0);
9539    }
9540
9541    #[test]
9542    fn test_working_memory_entry_byte_pairs_returns_correct_lengths() {
9543        let wm = WorkingMemory::new(10).unwrap();
9544        wm.set("ab", "hello").unwrap();
9545        let pairs = wm.entry_byte_pairs().unwrap();
9546        assert_eq!(pairs.len(), 1);
9547        assert_eq!(pairs[0], (2, 5));
9548    }
9549
9550    #[test]
9551    fn test_working_memory_entry_byte_pairs_empty_for_empty_store() {
9552        let wm = WorkingMemory::new(10).unwrap();
9553        assert!(wm.entry_byte_pairs().unwrap().is_empty());
9554    }
9555
9556    #[test]
9557    fn test_working_memory_key_with_longest_value_returns_correct_key() {
9558        let wm = WorkingMemory::new(10).unwrap();
9559        wm.set("k1", "short").unwrap();
9560        wm.set("k2", "much longer value here").unwrap();
9561        assert_eq!(wm.key_with_longest_value().unwrap(), Some("k2".to_string()));
9562    }
9563
9564    #[test]
9565    fn test_working_memory_key_with_longest_value_returns_none_when_empty() {
9566        let wm = WorkingMemory::new(10).unwrap();
9567        assert_eq!(wm.key_with_longest_value().unwrap(), None);
9568    }
9569
9570    // ── Round 44: key_value_pairs_above_length ─────────────────────────────────
9571
9572    #[test]
9573    fn test_key_value_pairs_above_length_returns_matching_pairs() {
9574        let wm = WorkingMemory::new(100).unwrap();
9575        wm.set("short", "hi").unwrap();
9576        wm.set("long", "hello world").unwrap();
9577        let pairs = wm.key_value_pairs_above_length(5).unwrap();
9578        assert_eq!(pairs.len(), 1);
9579        assert_eq!(pairs[0].0, "long");
9580        assert_eq!(pairs[0].1, "hello world");
9581    }
9582
9583    #[test]
9584    fn test_key_value_pairs_above_length_empty_when_none_qualify() {
9585        let wm = WorkingMemory::new(100).unwrap();
9586        wm.set("a", "x").unwrap();
9587        assert!(wm.key_value_pairs_above_length(100).unwrap().is_empty());
9588    }
9589
9590    #[test]
9591    fn test_key_value_pairs_above_length_empty_for_empty_store() {
9592        let wm = WorkingMemory::new(100).unwrap();
9593        assert!(wm.key_value_pairs_above_length(0).unwrap().is_empty());
9594    }
9595
9596    // ── Round 45: episode_ids, total_key_bytes ─────────────────────────────────
9597
9598    #[test]
9599    fn test_episodic_store_episode_ids_returns_all_ids() {
9600        let store = EpisodicStore::new();
9601        let agent = AgentId::new("a");
9602        let id1 = store.add_episode(agent.clone(), "first", 0.5).unwrap();
9603        let id2 = store.add_episode(agent.clone(), "second", 0.5).unwrap();
9604        let mut ids = store.episode_ids(&agent).unwrap();
9605        ids.sort_unstable_by(|a, b| a.as_str().cmp(b.as_str()));
9606        assert!(ids.contains(&id1));
9607        assert!(ids.contains(&id2));
9608        assert_eq!(ids.len(), 2);
9609    }
9610
9611    #[test]
9612    fn test_episodic_store_episode_ids_empty_for_unknown_agent() {
9613        let store = EpisodicStore::new();
9614        assert!(store.episode_ids(&AgentId::new("nobody")).unwrap().is_empty());
9615    }
9616
9617    // ── Round 45: episode_count_all_agents, increment ─────────────────────────
9618
9619    #[test]
9620    fn test_episode_count_all_agents_sums_across_agents() {
9621        let store = EpisodicStore::new();
9622        let a = AgentId::new("r45-cnt-a");
9623        let b = AgentId::new("r45-cnt-b");
9624        store.add_episode(a.clone(), "ep1", 0.5).unwrap();
9625        store.add_episode(a.clone(), "ep2", 0.5).unwrap();
9626        store.add_episode(b.clone(), "ep3", 0.5).unwrap();
9627        assert_eq!(store.episode_count_all_agents().unwrap(), 3);
9628    }
9629
9630    #[test]
9631    fn test_episode_count_all_agents_zero_for_empty_store() {
9632        let store = EpisodicStore::new();
9633        assert_eq!(store.episode_count_all_agents().unwrap(), 0);
9634    }
9635
9636    #[test]
9637    fn test_working_memory_increment_inserts_zero_when_absent() {
9638        let wm = WorkingMemory::new(100).unwrap();
9639        let v = wm.increment("counter-r45", 5).unwrap();
9640        assert_eq!(v, 5);
9641        assert_eq!(wm.get("counter-r45").unwrap().unwrap(), "5");
9642    }
9643
9644    #[test]
9645    fn test_working_memory_increment_adds_to_existing_value() {
9646        let wm = WorkingMemory::new(100).unwrap();
9647        wm.set("n-r45", "10").unwrap();
9648        let v = wm.increment("n-r45", 3).unwrap();
9649        assert_eq!(v, 13);
9650    }
9651
9652    #[test]
9653    fn test_working_memory_increment_returns_error_for_non_integer_value() {
9654        let wm = WorkingMemory::new(100).unwrap();
9655        wm.set("bad-r45", "not_a_number").unwrap();
9656        assert!(wm.increment("bad-r45", 1).is_err());
9657    }
9658
9659    #[test]
9660    fn test_working_memory_increment_negative_step() {
9661        let wm = WorkingMemory::new(100).unwrap();
9662        wm.set("n2-r45", "10").unwrap();
9663        let v = wm.increment("n2-r45", -4).unwrap();
9664        assert_eq!(v, 6);
9665    }
9666
9667    // ── Round 45: all_episode_ids, non_empty_values ───────────────────────────
9668
9669    #[test]
9670    fn test_all_episode_ids_returns_ids_across_agents() {
9671        let store = EpisodicStore::new();
9672        let a = AgentId::new("r45-all-a");
9673        let b = AgentId::new("r45-all-b");
9674        let id1 = store.add_episode(a.clone(), "ep1", 0.5).unwrap();
9675        let id2 = store.add_episode(b.clone(), "ep2", 0.5).unwrap();
9676        let ids = store.all_episode_ids().unwrap();
9677        assert!(ids.contains(&id1));
9678        assert!(ids.contains(&id2));
9679        assert_eq!(ids.len(), 2);
9680    }
9681
9682    #[test]
9683    fn test_all_episode_ids_empty_for_empty_store() {
9684        let store = EpisodicStore::new();
9685        assert!(store.all_episode_ids().unwrap().is_empty());
9686    }
9687
9688    #[test]
9689    fn test_working_memory_non_empty_values_filters_empty() {
9690        let wm = WorkingMemory::new(100).unwrap();
9691        wm.set("k1-r45", "hello").unwrap();
9692        wm.set("k2-r45", "").unwrap();
9693        wm.set("k3-r45", "world").unwrap();
9694        let vals = wm.non_empty_values().unwrap();
9695        assert_eq!(vals.len(), 2);
9696        assert!(vals.contains(&"hello".to_string()));
9697        assert!(vals.contains(&"world".to_string()));
9698    }
9699
9700    #[test]
9701    fn test_working_memory_non_empty_values_empty_for_empty_store() {
9702        let wm = WorkingMemory::new(100).unwrap();
9703        assert!(wm.non_empty_values().unwrap().is_empty());
9704    }
9705
9706    // ── Round 46: episodes_above_importance, total_bytes ──────────────────────
9707
9708    #[test]
9709    fn test_episodic_store_episodes_above_importance_filters_correctly() {
9710        let store = EpisodicStore::new();
9711        let agent = AgentId::new("r46-a");
9712        store.add_episode(agent.clone(), "low", 0.3).unwrap();
9713        store.add_episode(agent.clone(), "high", 0.9).unwrap();
9714        let result = store.episodes_above_importance(&agent, 0.5).unwrap();
9715        assert_eq!(result.len(), 1);
9716        assert_eq!(result[0].content, "high");
9717    }
9718
9719    #[test]
9720    fn test_episodic_store_episodes_above_importance_empty_for_unknown_agent() {
9721        let store = EpisodicStore::new();
9722        assert!(store.episodes_above_importance(&AgentId::new("nobody-r46"), 0.5).unwrap().is_empty());
9723    }
9724
9725    #[test]
9726    fn test_working_memory_total_bytes_sums_key_and_value_lengths() {
9727        let wm = WorkingMemory::new(10).unwrap();
9728        wm.set("ab", "xyz").unwrap(); // key=2, value=3 → total=5
9729        assert_eq!(wm.total_bytes().unwrap(), 5);
9730    }
9731
9732    #[test]
9733    fn test_working_memory_total_bytes_zero_for_empty_store() {
9734        let wm = WorkingMemory::new(10).unwrap();
9735        assert_eq!(wm.total_bytes().unwrap(), 0);
9736    }
9737
9738    // ── Round 47: episodes_between, max_key_bytes, value_length_histogram ─────
9739
9740    #[test]
9741    fn test_episodic_store_episodes_between_returns_in_range() {
9742        use chrono::{Duration, Utc};
9743        let store = EpisodicStore::new();
9744        let agent = AgentId::new("r47-eb");
9745        store.add_episode(agent.clone(), "old", 0.5).unwrap();
9746        let past = Utc::now() - Duration::hours(1);
9747        let future = Utc::now() + Duration::hours(1);
9748        let result = store.episodes_between(&agent, past, future).unwrap();
9749        assert_eq!(result.len(), 1);
9750    }
9751
9752    #[test]
9753    fn test_episodic_store_episodes_between_empty_for_out_of_range() {
9754        use chrono::{Duration, Utc};
9755        let store = EpisodicStore::new();
9756        let agent = AgentId::new("r47-eb2");
9757        store.add_episode(agent.clone(), "ep", 0.5).unwrap();
9758        let far_past_start = Utc::now() - Duration::hours(10);
9759        let far_past_end = Utc::now() - Duration::hours(5);
9760        assert!(store.episodes_between(&agent, far_past_start, far_past_end).unwrap().is_empty());
9761    }
9762
9763    #[test]
9764    fn test_working_memory_max_key_bytes_returns_longest_key_len() {
9765        let wm = WorkingMemory::new(10).unwrap();
9766        wm.set("ab", "x").unwrap();
9767        wm.set("abcde", "y").unwrap();
9768        assert_eq!(wm.max_key_bytes().unwrap(), 5);
9769    }
9770
9771    #[test]
9772    fn test_working_memory_max_key_bytes_zero_for_empty_store() {
9773        let wm = WorkingMemory::new(10).unwrap();
9774        assert_eq!(wm.max_key_bytes().unwrap(), 0);
9775    }
9776
9777    #[test]
9778    fn test_working_memory_value_length_histogram_buckets_values() {
9779        let wm = WorkingMemory::new(10).unwrap();
9780        wm.set("k1", "ab").unwrap();   // len=2, bucket 0 (size=5)
9781        wm.set("k2", "abcde").unwrap(); // len=5, bucket 5
9782        wm.set("k3", "ab").unwrap();   // len=2, bucket 0 — evicted k1 under capacity
9783        // actually with capacity=10, k1 k2 k3 all fit
9784        let hist = wm.value_length_histogram(5).unwrap();
9785        // bucket 0: values with len 0-4 (both "ab"s have len 2)
9786        // but k3 set after k1 so k1 still exists (both are set under capacity 10)
9787        // Wait, k3 with same key "k3" and k1 with key "k1" — different keys
9788        // k1="ab"(2), k2="abcde"(5), k3="ab"(2)
9789        // bucket 0 (0-4): 2 entries; bucket 5 (5-9): 1 entry
9790        assert!(!hist.is_empty());
9791    }
9792
9793    #[test]
9794    fn test_working_memory_value_length_histogram_empty_for_zero_bucket_size() {
9795        let wm = WorkingMemory::new(10).unwrap();
9796        wm.set("k", "v").unwrap();
9797        assert!(wm.value_length_histogram(0).unwrap().is_empty());
9798    }
9799
9800    // ── Round 48: weighted_importance_sum, episode_content_lengths, semantic_key_count ──
9801
9802    #[test]
9803    fn test_episodic_store_weighted_importance_sum_sums_importances() {
9804        let store = EpisodicStore::new();
9805        let agent = AgentId::new("r48-wis");
9806        store.add_episode(agent.clone(), "ep1", 0.3).unwrap();
9807        store.add_episode(agent.clone(), "ep2", 0.7).unwrap();
9808        let sum = store.weighted_importance_sum(&agent).unwrap();
9809        assert!((sum - 1.0).abs() < 1e-5);
9810    }
9811
9812    #[test]
9813    fn test_episodic_store_weighted_importance_sum_zero_for_unknown_agent() {
9814        let store = EpisodicStore::new();
9815        let agent = AgentId::new("r48-wis-missing");
9816        assert_eq!(store.weighted_importance_sum(&agent).unwrap(), 0.0);
9817    }
9818
9819    #[test]
9820    fn test_episodic_store_episode_content_lengths_returns_lengths() {
9821        let store = EpisodicStore::new();
9822        let agent = AgentId::new("r48-ecl");
9823        store.add_episode(agent.clone(), "hi", 0.5).unwrap();
9824        store.add_episode(agent.clone(), "hello", 0.5).unwrap();
9825        let mut lengths = store.episode_content_lengths(&agent).unwrap();
9826        lengths.sort_unstable();
9827        assert_eq!(lengths, vec![2, 5]);
9828    }
9829
9830    #[test]
9831    fn test_episodic_store_episode_content_lengths_empty_for_unknown_agent() {
9832        let store = EpisodicStore::new();
9833        let agent = AgentId::new("r48-ecl-missing");
9834        assert!(store.episode_content_lengths(&agent).unwrap().is_empty());
9835    }
9836
9837    #[test]
9838    fn test_working_memory_semantic_key_count_counts_matching_keys() {
9839        let wm = WorkingMemory::new(10).unwrap();
9840        wm.set("intent_primary", "v1").unwrap();
9841        wm.set("intent_secondary", "v2").unwrap();
9842        wm.set("goal_main", "v3").unwrap();
9843        assert_eq!(wm.semantic_key_count("intent").unwrap(), 2);
9844    }
9845
9846    #[test]
9847    fn test_working_memory_semantic_key_count_zero_when_no_match() {
9848        let wm = WorkingMemory::new(10).unwrap();
9849        wm.set("context_a", "v").unwrap();
9850        assert_eq!(wm.semantic_key_count("intent").unwrap(), 0);
9851    }
9852
9853    // ── Round 49: episode_count_by_agent, longest_value_bytes ─────────────────
9854
9855    #[test]
9856    fn test_episode_count_by_agent_maps_agents_to_counts() {
9857        let store = EpisodicStore::new();
9858        let a1 = AgentId::new("r49-ecba-1");
9859        let a2 = AgentId::new("r49-ecba-2");
9860        store.add_episode(a1.clone(), "ep1", 0.5).unwrap();
9861        store.add_episode(a1.clone(), "ep2", 0.5).unwrap();
9862        store.add_episode(a2.clone(), "ep3", 0.5).unwrap();
9863        let counts = store.episode_count_by_agent().unwrap();
9864        assert_eq!(*counts.get(&a1).unwrap(), 2);
9865        assert_eq!(*counts.get(&a2).unwrap(), 1);
9866    }
9867
9868    #[test]
9869    fn test_episode_count_by_agent_empty_for_empty_store() {
9870        let store = EpisodicStore::new();
9871        assert!(store.episode_count_by_agent().unwrap().is_empty());
9872    }
9873
9874    #[test]
9875    fn test_working_memory_longest_value_bytes_returns_max_value_len() {
9876        let wm = WorkingMemory::new(10).unwrap();
9877        wm.set("k1", "ab").unwrap();
9878        wm.set("k2", "abcde").unwrap();
9879        assert_eq!(wm.longest_value_bytes().unwrap(), 5);
9880    }
9881
9882    #[test]
9883    fn test_working_memory_longest_value_bytes_zero_for_empty_store() {
9884        let wm = WorkingMemory::new(10).unwrap();
9885        assert_eq!(wm.longest_value_bytes().unwrap(), 0);
9886    }
9887
9888    // ── Round 47: episodes_between, max_key_bytes, value_length_histogram,
9889    //              episodes_tagged_with_all, content_word_count_total ──────────
9890
9891    #[test]
9892    fn test_episodes_between_returns_episodes_in_range() {
9893        use chrono::{TimeZone, Utc};
9894        let store = EpisodicStore::new();
9895        let agent = AgentId::new("r47-eb-a");
9896        let t0 = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
9897        let t1 = Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap();
9898        let t2 = Utc.with_ymd_and_hms(2024, 1, 3, 0, 0, 0).unwrap();
9899        store.add_episode_at(agent.clone(), "early", 0.5, t0).unwrap();
9900        store.add_episode_at(agent.clone(), "mid", 0.5, t1).unwrap();
9901        store.add_episode_at(agent.clone(), "late", 0.5, t2).unwrap();
9902        // [t0, t2) — excludes t2
9903        let result = store.episodes_between(&agent, t0, t2).unwrap();
9904        assert_eq!(result.len(), 2);
9905    }
9906
9907    #[test]
9908    fn test_episodes_between_returns_empty_for_unknown_agent() {
9909        use chrono::{TimeZone, Utc};
9910        let store = EpisodicStore::new();
9911        let t0 = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
9912        let t1 = Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap();
9913        assert!(
9914            store
9915                .episodes_between(&AgentId::new("nobody-r47"), t0, t1)
9916                .unwrap()
9917                .is_empty()
9918        );
9919    }
9920
9921    #[test]
9922    fn test_working_memory_max_key_bytes_returns_longest_key_length() {
9923        let wm = WorkingMemory::new(10).unwrap();
9924        wm.set("ab", "v").unwrap();        // 2 bytes
9925        wm.set("abcde", "v").unwrap();     // 5 bytes
9926        assert_eq!(wm.max_key_bytes().unwrap(), 5);
9927    }
9928
9929    #[test]
9930    fn test_working_memory_max_key_bytes_zero_for_empty_store_r47() {
9931        let wm = WorkingMemory::new(10).unwrap();
9932        assert_eq!(wm.max_key_bytes().unwrap(), 0);
9933    }
9934
9935    #[test]
9936    fn test_working_memory_value_length_histogram_groups_by_bucket() {
9937        let wm = WorkingMemory::new(10).unwrap();
9938        wm.set("k1", "ab").unwrap();    // len=2 → bucket 0 (2/10*10=0)
9939        wm.set("k2", "abcde").unwrap(); // len=5 → bucket 0
9940        wm.set("k3", "abcdefghijk").unwrap(); // len=11 → bucket 10
9941        let hist = wm.value_length_histogram(10).unwrap();
9942        // bucket 0 → 2 entries, bucket 10 → 1 entry
9943        assert_eq!(hist.len(), 2);
9944        assert_eq!(hist[0], (0, 2));
9945        assert_eq!(hist[1], (10, 1));
9946    }
9947
9948    #[test]
9949    fn test_working_memory_value_length_histogram_empty_for_zero_bucket_size_r47() {
9950        let wm = WorkingMemory::new(10).unwrap();
9951        wm.set("k", "v").unwrap();
9952        assert!(wm.value_length_histogram(0).unwrap().is_empty());
9953    }
9954
9955    #[test]
9956    fn test_episodes_tagged_with_all_returns_matching_episodes() {
9957        let store = EpisodicStore::new();
9958        let agent = AgentId::new("r47-twa-a");
9959        store
9960            .add_episode_with_tags(agent.clone(), "both", 0.5, vec!["x".into(), "y".into()])
9961            .unwrap();
9962        store
9963            .add_episode_with_tags(agent.clone(), "only_x", 0.5, vec!["x".into()])
9964            .unwrap();
9965        let result = store.episodes_tagged_with_all(&agent, &["x", "y"]).unwrap();
9966        assert_eq!(result.len(), 1);
9967        assert_eq!(result[0].content, "both");
9968    }
9969
9970    #[test]
9971    fn test_episodes_tagged_with_all_empty_tags_returns_empty() {
9972        let store = EpisodicStore::new();
9973        let agent = AgentId::new("r47-twa-b");
9974        store.add_episode(agent.clone(), "ep", 0.5).unwrap();
9975        assert!(store.episodes_tagged_with_all(&agent, &[]).unwrap().is_empty());
9976    }
9977
9978    #[test]
9979    fn test_content_word_count_total_sums_across_episodes() {
9980        let store = EpisodicStore::new();
9981        let agent = AgentId::new("r47-cwct-a");
9982        store.add_episode(agent.clone(), "one two three", 0.5).unwrap(); // 3 words
9983        store.add_episode(agent.clone(), "four five", 0.5).unwrap();     // 2 words
9984        assert_eq!(store.content_word_count_total(&agent).unwrap(), 5);
9985    }
9986
9987    #[test]
9988    fn test_content_word_count_total_zero_for_unknown_agent() {
9989        let store = EpisodicStore::new();
9990        assert_eq!(
9991            store
9992                .content_word_count_total(&AgentId::new("nobody-r47-cwct"))
9993                .unwrap(),
9994            0
9995        );
9996    }
9997
9998    // ── Round 49: agents_sorted_by_episode_count, episodes_with_min_word_count,
9999    //              values_sorted ───────────────────────────────────────────────
10000
10001    #[test]
10002    fn test_agents_sorted_by_episode_count_most_episodes_first() {
10003        let store = EpisodicStore::new();
10004        let a = AgentId::new("r49-asbec-a");
10005        let b = AgentId::new("r49-asbec-b");
10006        store.add_episode(a.clone(), "e1", 0.5).unwrap();
10007        store.add_episode(b.clone(), "e2", 0.5).unwrap();
10008        store.add_episode(b.clone(), "e3", 0.5).unwrap();
10009        let sorted = store.agents_sorted_by_episode_count().unwrap();
10010        assert_eq!(sorted[0].as_str(), b.as_str());
10011        assert_eq!(sorted[1].as_str(), a.as_str());
10012    }
10013
10014    #[test]
10015    fn test_agents_sorted_by_episode_count_empty_for_empty_store() {
10016        let store = EpisodicStore::new();
10017        assert!(store.agents_sorted_by_episode_count().unwrap().is_empty());
10018    }
10019
10020    #[test]
10021    fn test_episodes_with_min_word_count_returns_qualifying_episodes() {
10022        let store = EpisodicStore::new();
10023        let agent = AgentId::new("r49-ewmwc-a");
10024        store.add_episode(agent.clone(), "one", 0.5).unwrap();            // 1 word
10025        store.add_episode(agent.clone(), "one two three", 0.5).unwrap();   // 3 words
10026        let result = store.episodes_with_min_word_count(&agent, 2).unwrap();
10027        assert_eq!(result.len(), 1);
10028        assert_eq!(result[0].content, "one two three");
10029    }
10030
10031    #[test]
10032    fn test_episodes_with_min_word_count_empty_for_unknown_agent() {
10033        let store = EpisodicStore::new();
10034        assert!(
10035            store
10036                .episodes_with_min_word_count(&AgentId::new("nobody-r49"), 1)
10037                .unwrap()
10038                .is_empty()
10039        );
10040    }
10041
10042    #[test]
10043    fn test_working_memory_values_sorted_returns_alphabetically_sorted_values() {
10044        let wm = WorkingMemory::new(10).unwrap();
10045        wm.set("k1", "banana").unwrap();
10046        wm.set("k2", "apple").unwrap();
10047        wm.set("k3", "cherry").unwrap();
10048        let sorted = wm.values_sorted().unwrap();
10049        assert_eq!(sorted, vec!["apple", "banana", "cherry"]);
10050    }
10051
10052    #[test]
10053    fn test_working_memory_values_sorted_empty_for_empty_store() {
10054        let wm = WorkingMemory::new(10).unwrap();
10055        assert!(wm.values_sorted().unwrap().is_empty());
10056    }
10057
10058    // ── Round 50: episodes_above_content_bytes, value_count_above_bytes ────────
10059
10060    #[test]
10061    fn test_episodes_above_content_bytes_returns_long_episodes() {
10062        let store = EpisodicStore::new();
10063        let agent = AgentId::new("r50-eacb");
10064        store.add_episode(agent.clone(), "hi", 0.5).unwrap();
10065        store.add_episode(agent.clone(), "long content here", 0.5).unwrap();
10066        let result = store.episodes_above_content_bytes(&agent, 5).unwrap();
10067        assert_eq!(result.len(), 1);
10068        assert_eq!(result[0].content, "long content here");
10069    }
10070
10071    #[test]
10072    fn test_episodes_above_content_bytes_empty_for_unknown_agent() {
10073        let store = EpisodicStore::new();
10074        let agent = AgentId::new("r50-eacb-missing");
10075        assert!(store.episodes_above_content_bytes(&agent, 0).unwrap().is_empty());
10076    }
10077
10078    #[test]
10079    fn test_value_count_above_bytes_counts_long_values() {
10080        let wm = WorkingMemory::new(10).unwrap();
10081        wm.set("k1", "ab").unwrap();
10082        wm.set("k2", "abcdef").unwrap();
10083        assert_eq!(wm.value_count_above_bytes(3).unwrap(), 1);
10084    }
10085
10086    #[test]
10087    fn test_value_count_above_bytes_zero_for_empty_store() {
10088        let wm = WorkingMemory::new(10).unwrap();
10089        assert_eq!(wm.value_count_above_bytes(0).unwrap(), 0);
10090    }
10091
10092    // ── Round 47: most_important_episode, count_keys_above_bytes ──────────────
10093
10094    #[test]
10095    fn test_most_important_episode_returns_highest_importance() {
10096        let store = EpisodicStore::new();
10097        let agent = AgentId::new("r47-imp-a");
10098        store.add_episode(agent.clone(), "low", 0.2).unwrap();
10099        store.add_episode(agent.clone(), "high", 0.9).unwrap();
10100        let result = store.most_important_episode(&agent).unwrap();
10101        assert!(result.is_some());
10102        assert_eq!(result.unwrap().content, "high");
10103    }
10104
10105    #[test]
10106    fn test_most_important_episode_returns_none_for_unknown_agent() {
10107        let store = EpisodicStore::new();
10108        assert!(store.most_important_episode(&AgentId::new("nobody-r47")).unwrap().is_none());
10109    }
10110
10111    #[test]
10112    fn test_count_keys_above_bytes_counts_long_keys() {
10113        let wm = WorkingMemory::new(100).unwrap();
10114        wm.set("ab", "v").unwrap();       // key len=2
10115        wm.set("abcdef", "v").unwrap();   // key len=6
10116        assert_eq!(wm.count_keys_above_bytes(3).unwrap(), 1);
10117    }
10118
10119    #[test]
10120    fn test_count_keys_above_bytes_zero_for_empty_store() {
10121        let wm = WorkingMemory::new(100).unwrap();
10122        assert_eq!(wm.count_keys_above_bytes(0).unwrap(), 0);
10123    }
10124
10125    // ── Round 50: most_tagged_episode, tag_frequency, agents_sorted_by_episode_count,
10126    //              values_sorted ───────────────────────────────────────────────
10127
10128    #[test]
10129    fn test_most_tagged_episode_returns_episode_with_most_tags() {
10130        let store = EpisodicStore::new();
10131        let agent = AgentId::new("r50-mte-a");
10132        store
10133            .add_episode_with_tags(agent.clone(), "many", 0.5, vec!["a".into(), "b".into(), "c".into()])
10134            .unwrap();
10135        store
10136            .add_episode_with_tags(agent.clone(), "few", 0.5, vec!["a".into()])
10137            .unwrap();
10138        let result = store.most_tagged_episode(&agent).unwrap().unwrap();
10139        assert_eq!(result.content, "many");
10140    }
10141
10142    #[test]
10143    fn test_most_tagged_episode_none_for_unknown_agent() {
10144        let store = EpisodicStore::new();
10145        assert!(store.most_tagged_episode(&AgentId::new("nobody-r50")).unwrap().is_none());
10146    }
10147
10148    #[test]
10149    fn test_tag_frequency_counts_each_tag() {
10150        let store = EpisodicStore::new();
10151        let agent = AgentId::new("r50-tf-a");
10152        store
10153            .add_episode_with_tags(agent.clone(), "e1", 0.5, vec!["x".into(), "y".into()])
10154            .unwrap();
10155        store
10156            .add_episode_with_tags(agent.clone(), "e2", 0.5, vec!["x".into()])
10157            .unwrap();
10158        let freq = store.tag_frequency(&agent).unwrap();
10159        assert_eq!(freq["x"], 2);
10160        assert_eq!(freq["y"], 1);
10161    }
10162
10163    #[test]
10164    fn test_tag_frequency_empty_for_unknown_agent() {
10165        let store = EpisodicStore::new();
10166        assert!(store.tag_frequency(&AgentId::new("nobody-r50-tf")).unwrap().is_empty());
10167    }
10168
10169    // ── Round 51: unique_agents_count, key_value_pairs_sorted ─────────────────
10170
10171    #[test]
10172    fn test_unique_agents_count_reflects_distinct_agents() {
10173        let store = EpisodicStore::new();
10174        let a1 = AgentId::new("r51-uac-1");
10175        let a2 = AgentId::new("r51-uac-2");
10176        store.add_episode(a1.clone(), "ep", 0.5).unwrap();
10177        store.add_episode(a2.clone(), "ep", 0.5).unwrap();
10178        assert_eq!(store.unique_agents_count().unwrap(), 2);
10179    }
10180
10181    #[test]
10182    fn test_unique_agents_count_zero_for_empty_store() {
10183        let store = EpisodicStore::new();
10184        assert_eq!(store.unique_agents_count().unwrap(), 0);
10185    }
10186
10187    #[test]
10188    fn test_key_value_pairs_sorted_returns_alphabetical_pairs() {
10189        let wm = WorkingMemory::new(10).unwrap();
10190        wm.set("b_key", "v2").unwrap();
10191        wm.set("a_key", "v1").unwrap();
10192        let pairs = wm.key_value_pairs_sorted().unwrap();
10193        assert_eq!(pairs[0].0, "a_key");
10194        assert_eq!(pairs[1].0, "b_key");
10195    }
10196
10197    #[test]
10198    fn test_key_value_pairs_sorted_empty_for_empty_store() {
10199        let wm = WorkingMemory::new(10).unwrap();
10200        assert!(wm.key_value_pairs_sorted().unwrap().is_empty());
10201    }
10202
10203    // ── Round 52 ──────────────────────────────────────────────────────────────
10204
10205    #[test]
10206    fn test_episode_count_with_tag_counts_matching_episodes() {
10207        let store = EpisodicStore::new();
10208        let agent = AgentId::new("a1");
10209        store
10210            .add_episode_with_tags(agent.clone(), "content", 0.5, vec!["rust".into(), "ai".into()])
10211            .unwrap();
10212        store
10213            .add_episode_with_tags(agent.clone(), "other", 0.3, vec!["rust".into()])
10214            .unwrap();
10215        assert_eq!(store.episode_count_with_tag(&agent, "rust").unwrap(), 2);
10216        assert_eq!(store.episode_count_with_tag(&agent, "ai").unwrap(), 1);
10217    }
10218
10219    #[test]
10220    fn test_episode_count_with_tag_zero_for_unknown_agent() {
10221        let store = EpisodicStore::new();
10222        let agent = AgentId::new("unknown");
10223        assert_eq!(store.episode_count_with_tag(&agent, "rust").unwrap(), 0);
10224    }
10225
10226    #[test]
10227    fn test_has_duplicate_values_true_when_two_keys_share_value() {
10228        let wm = WorkingMemory::new(10).unwrap();
10229        wm.set("k1", "same").unwrap();
10230        wm.set("k2", "same").unwrap();
10231        assert!(wm.has_duplicate_values().unwrap());
10232    }
10233
10234    #[test]
10235    fn test_has_duplicate_values_false_for_unique_values() {
10236        let wm = WorkingMemory::new(10).unwrap();
10237        wm.set("k1", "one").unwrap();
10238        wm.set("k2", "two").unwrap();
10239        assert!(!wm.has_duplicate_values().unwrap());
10240    }
10241
10242    #[test]
10243    fn test_has_duplicate_values_false_for_empty_store() {
10244        let wm = WorkingMemory::new(10).unwrap();
10245        assert!(!wm.has_duplicate_values().unwrap());
10246    }
10247
10248    // ── Round 52: episodes_with_tag_count, has_key_starting_with ──────────────
10249
10250    #[test]
10251    fn test_episodes_with_tag_count_counts_tagged_items() {
10252        let store = EpisodicStore::new();
10253        let agent = AgentId::new("r52-ewtc-1");
10254        store.add_episode(agent.clone(), "no tags episode", 0.5).unwrap();
10255        store
10256            .add_episode_with_tags(agent.clone(), "tagged content", 0.8, vec!["relevant".to_string()])
10257            .unwrap();
10258        assert_eq!(store.episodes_with_tag_count(&agent).unwrap(), 1);
10259    }
10260
10261    #[test]
10262    fn test_episodes_with_tag_count_zero_for_unknown_agent() {
10263        let store = EpisodicStore::new();
10264        let agent = AgentId::new("r52-ewtc-unknown");
10265        assert_eq!(store.episodes_with_tag_count(&agent).unwrap(), 0);
10266    }
10267
10268    #[test]
10269    fn test_has_key_starting_with_true_when_prefix_matches() {
10270        let wm = WorkingMemory::new(10).unwrap();
10271        wm.set("config_timeout", "30").unwrap();
10272        assert!(wm.has_key_starting_with("config_").unwrap());
10273    }
10274
10275    #[test]
10276    fn test_has_key_starting_with_false_when_no_match() {
10277        let wm = WorkingMemory::new(10).unwrap();
10278        wm.set("data_x", "v").unwrap();
10279        assert!(!wm.has_key_starting_with("config_").unwrap());
10280    }
10281
10282    #[test]
10283    fn test_has_key_starting_with_false_for_empty_store() {
10284        let wm = WorkingMemory::new(10).unwrap();
10285        assert!(!wm.has_key_starting_with("any").unwrap());
10286    }
10287
10288    // ── Round 53: newest_episode, value_bytes_total, all_keys_sorted ──────────
10289
10290    #[test]
10291    fn test_newest_episode_returns_most_recent() {
10292        let store = EpisodicStore::new();
10293        let agent = AgentId::new("r53-ne-1");
10294        store.add_episode(agent.clone(), "first", 0.5).unwrap();
10295        store.add_episode(agent.clone(), "second", 0.7).unwrap();
10296        let newest = store.newest_episode(&agent).unwrap();
10297        assert!(newest.is_some());
10298        assert_eq!(newest.unwrap().content, "second");
10299    }
10300
10301    #[test]
10302    fn test_newest_episode_none_for_unknown_agent() {
10303        let store = EpisodicStore::new();
10304        let agent = AgentId::new("r53-ne-unknown");
10305        assert!(store.newest_episode(&agent).unwrap().is_none());
10306    }
10307
10308    #[test]
10309    fn test_value_bytes_total_sums_value_lengths() {
10310        let wm = WorkingMemory::new(10).unwrap();
10311        wm.set("k1", "abc").unwrap();
10312        wm.set("k2", "de").unwrap();
10313        assert_eq!(wm.value_bytes_total().unwrap(), 5);
10314    }
10315
10316    #[test]
10317    fn test_value_bytes_total_zero_for_empty_store() {
10318        let wm = WorkingMemory::new(10).unwrap();
10319        assert_eq!(wm.value_bytes_total().unwrap(), 0);
10320    }
10321
10322    #[test]
10323    fn test_all_keys_sorted_returns_alphabetical_keys() {
10324        let wm = WorkingMemory::new(10).unwrap();
10325        wm.set("zebra", "v").unwrap();
10326        wm.set("alpha", "v").unwrap();
10327        wm.set("mango", "v").unwrap();
10328        let keys = wm.all_keys_sorted().unwrap();
10329        assert_eq!(keys, vec!["alpha", "mango", "zebra"]);
10330    }
10331
10332    #[test]
10333    fn test_all_keys_sorted_empty_for_empty_store() {
10334        let wm = WorkingMemory::new(10).unwrap();
10335        assert!(wm.all_keys_sorted().unwrap().is_empty());
10336    }
10337
10338    // ── Round 53 ──────────────────────────────────────────────────────────────
10339
10340    #[test]
10341    fn test_shortest_value_returns_smallest_value() {
10342        let wm = WorkingMemory::new(10).unwrap();
10343        wm.set("k1", "hi").unwrap();
10344        wm.set("k2", "hello").unwrap();
10345        assert_eq!(wm.shortest_value().unwrap(), Some("hi".to_string()));
10346    }
10347
10348    #[test]
10349    fn test_shortest_value_none_for_empty_store() {
10350        let wm = WorkingMemory::new(10).unwrap();
10351        assert_eq!(wm.shortest_value().unwrap(), None);
10352    }
10353
10354    #[test]
10355    fn test_shortest_value_tie_broken_lexicographically() {
10356        let wm = WorkingMemory::new(10).unwrap();
10357        wm.set("k1", "bb").unwrap();
10358        wm.set("k2", "aa").unwrap();
10359        assert_eq!(wm.shortest_value().unwrap(), Some("aa".to_string()));
10360    }
10361
10362    // ── Round 48 ──────────────────────────────────────────────────────────────
10363
10364    #[test]
10365    fn test_key_count_starting_with_counts_matching_keys() {
10366        let wm = WorkingMemory::new(10).unwrap();
10367        wm.set("cfg_host", "localhost").unwrap();
10368        wm.set("cfg_port", "8080").unwrap();
10369        wm.set("debug_mode", "true").unwrap();
10370        assert_eq!(wm.key_count_starting_with("cfg_").unwrap(), 2);
10371    }
10372
10373    #[test]
10374    fn test_key_count_starting_with_zero_when_no_match() {
10375        let wm = WorkingMemory::new(10).unwrap();
10376        wm.set("alpha", "v").unwrap();
10377        assert_eq!(wm.key_count_starting_with("zzz").unwrap(), 0);
10378    }
10379
10380    #[test]
10381    fn test_key_count_starting_with_zero_for_empty_store() {
10382        let wm = WorkingMemory::new(10).unwrap();
10383        assert_eq!(wm.key_count_starting_with("any").unwrap(), 0);
10384    }
10385
10386    // ── Round 54: agent_ids_with_episodes, episode_importance_avg, key_count, value_for_key_or_default ──
10387
10388    #[test]
10389    fn test_agent_ids_with_episodes_returns_sorted_ids() {
10390        let store = EpisodicStore::new();
10391        let a1 = AgentId::new("r54-z-agent");
10392        let a2 = AgentId::new("r54-a-agent");
10393        store.add_episode(a1.clone(), "ep", 0.5).unwrap();
10394        store.add_episode(a2.clone(), "ep", 0.5).unwrap();
10395        let ids = store.agent_ids_with_episodes().unwrap();
10396        assert_eq!(ids.len(), 2);
10397        assert_eq!(ids[0].as_str(), "r54-a-agent");
10398        assert_eq!(ids[1].as_str(), "r54-z-agent");
10399    }
10400
10401    #[test]
10402    fn test_agent_ids_with_episodes_empty_for_empty_store() {
10403        let store = EpisodicStore::new();
10404        assert!(store.agent_ids_with_episodes().unwrap().is_empty());
10405    }
10406
10407    #[test]
10408    fn test_episode_importance_avg_returns_mean() {
10409        let store = EpisodicStore::new();
10410        let agent = AgentId::new("r54-eia-1");
10411        store.add_episode(agent.clone(), "ep1", 0.5).unwrap();
10412        store.add_episode(agent.clone(), "ep2", 1.0).unwrap();
10413        let avg = store.episode_importance_avg(&agent).unwrap();
10414        assert!((avg - 0.75).abs() < 1e-5);
10415    }
10416
10417    #[test]
10418    fn test_episode_importance_avg_zero_for_unknown_agent() {
10419        let store = EpisodicStore::new();
10420        let agent = AgentId::new("r54-eia-unknown");
10421        assert_eq!(store.episode_importance_avg(&agent).unwrap(), 0.0);
10422    }
10423
10424    #[test]
10425    fn test_key_count_returns_number_of_entries() {
10426        let wm = WorkingMemory::new(10).unwrap();
10427        wm.set("a", "1").unwrap();
10428        wm.set("b", "2").unwrap();
10429        assert_eq!(wm.key_count().unwrap(), 2);
10430    }
10431
10432    #[test]
10433    fn test_key_count_zero_for_empty_store() {
10434        let wm = WorkingMemory::new(10).unwrap();
10435        assert_eq!(wm.key_count().unwrap(), 0);
10436    }
10437
10438    #[test]
10439    fn test_value_for_key_or_default_returns_value_when_present() {
10440        let wm = WorkingMemory::new(10).unwrap();
10441        wm.set("host", "localhost").unwrap();
10442        assert_eq!(wm.value_for_key_or_default("host", "unknown").unwrap(), "localhost");
10443    }
10444
10445    #[test]
10446    fn test_value_for_key_or_default_returns_default_when_absent() {
10447        let wm = WorkingMemory::new(10).unwrap();
10448        assert_eq!(wm.value_for_key_or_default("missing", "fallback").unwrap(), "fallback");
10449    }
10450
10451    // ── Round 55: agent_episode_counts, episodes_for_agent_sorted_by_importance, entries_with_key_prefix ──
10452
10453    #[test]
10454    fn test_agent_episode_counts_returns_correct_counts() {
10455        let store = EpisodicStore::new();
10456        let a1 = AgentId::new("r55-aec-1");
10457        let a2 = AgentId::new("r55-aec-2");
10458        store.add_episode(a1.clone(), "ep1", 0.5).unwrap();
10459        store.add_episode(a1.clone(), "ep2", 0.7).unwrap();
10460        store.add_episode(a2.clone(), "ep3", 0.3).unwrap();
10461        let counts = store.agent_episode_counts().unwrap();
10462        assert_eq!(counts.get(&a1).copied(), Some(2));
10463        assert_eq!(counts.get(&a2).copied(), Some(1));
10464    }
10465
10466    #[test]
10467    fn test_agent_episode_counts_empty_for_empty_store() {
10468        let store = EpisodicStore::new();
10469        assert!(store.agent_episode_counts().unwrap().is_empty());
10470    }
10471
10472    #[test]
10473    fn test_episodes_for_agent_sorted_by_importance_descending() {
10474        let store = EpisodicStore::new();
10475        let agent = AgentId::new("r55-esbi-1");
10476        store.add_episode(agent.clone(), "low", 0.2).unwrap();
10477        store.add_episode(agent.clone(), "high", 0.9).unwrap();
10478        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
10479        let sorted = store.episodes_for_agent_sorted_by_importance(&agent).unwrap();
10480        assert_eq!(sorted[0].importance, 0.9);
10481        assert_eq!(sorted[2].importance, 0.2);
10482    }
10483
10484    #[test]
10485    fn test_entries_with_key_prefix_returns_matching_pairs() {
10486        let wm = WorkingMemory::new(10).unwrap();
10487        wm.set("cfg_host", "localhost").unwrap();
10488        wm.set("cfg_port", "8080").unwrap();
10489        wm.set("debug_mode", "true").unwrap();
10490        let entries = wm.entries_with_key_prefix("cfg_").unwrap();
10491        assert_eq!(entries.len(), 2);
10492        assert_eq!(entries[0].0, "cfg_host");
10493        assert_eq!(entries[1].0, "cfg_port");
10494    }
10495
10496    #[test]
10497    fn test_entries_with_key_prefix_empty_when_no_match() {
10498        let wm = WorkingMemory::new(10).unwrap();
10499        wm.set("data_x", "v").unwrap();
10500        assert!(wm.entries_with_key_prefix("cfg_").unwrap().is_empty());
10501    }
10502
10503    // ── Round 54 ──────────────────────────────────────────────────────────────
10504
10505    #[test]
10506    fn test_episodes_below_importance_filters_correctly() {
10507        let store = EpisodicStore::new();
10508        let agent = AgentId::new("a1");
10509        store.add_episode(agent.clone(), "low", 0.2).unwrap();
10510        store.add_episode(agent.clone(), "high", 0.9).unwrap();
10511        let low = store.episodes_below_importance(&agent, 0.5).unwrap();
10512        assert_eq!(low.len(), 1);
10513        assert_eq!(low[0].content, "low");
10514    }
10515
10516    #[test]
10517    fn test_episodes_below_importance_empty_for_unknown_agent() {
10518        let store = EpisodicStore::new();
10519        let agent = AgentId::new("unknown");
10520        assert!(store.episodes_below_importance(&agent, 0.5).unwrap().is_empty());
10521    }
10522
10523    #[test]
10524    fn test_keys_with_value_equal_to_returns_correct_keys() {
10525        let wm = WorkingMemory::new(10).unwrap();
10526        wm.set("k1", "hello").unwrap();
10527        wm.set("k2", "world").unwrap();
10528        wm.set("k3", "hello").unwrap();
10529        let mut keys = wm.keys_with_value_equal_to("hello").unwrap();
10530        keys.sort();
10531        assert_eq!(keys, vec!["k1", "k3"]);
10532    }
10533
10534    #[test]
10535    fn test_keys_with_value_equal_to_empty_when_no_match() {
10536        let wm = WorkingMemory::new(10).unwrap();
10537        wm.set("k1", "hello").unwrap();
10538        assert!(wm.keys_with_value_equal_to("world").unwrap().is_empty());
10539    }
10540
10541    // ── Round 49 ──────────────────────────────────────────────────────────────
10542
10543    #[test]
10544    fn test_all_values_sorted_returns_sorted_values() {
10545        let wm = WorkingMemory::new(10).unwrap();
10546        wm.set("k1", "banana").unwrap();
10547        wm.set("k2", "apple").unwrap();
10548        wm.set("k3", "cherry").unwrap();
10549        let values = wm.all_values_sorted().unwrap();
10550        assert_eq!(values, vec!["apple", "banana", "cherry"]);
10551    }
10552
10553    #[test]
10554    fn test_all_values_sorted_empty_for_empty_store() {
10555        let wm = WorkingMemory::new(10).unwrap();
10556        assert!(wm.all_values_sorted().unwrap().is_empty());
10557    }
10558
10559    #[test]
10560    fn test_keys_with_value_prefix_returns_matching_keys() {
10561        let wm = WorkingMemory::new(10).unwrap();
10562        wm.set("host", "http://example.com").unwrap();
10563        wm.set("api", "http://api.example.com").unwrap();
10564        wm.set("name", "alice").unwrap();
10565        let mut keys = wm.keys_with_value_prefix("http://").unwrap();
10566        keys.sort();
10567        assert_eq!(keys, vec!["api", "host"]);
10568    }
10569
10570    #[test]
10571    fn test_keys_with_value_prefix_empty_when_no_match() {
10572        let wm = WorkingMemory::new(10).unwrap();
10573        wm.set("k1", "hello").unwrap();
10574        assert!(wm.keys_with_value_prefix("zzz").unwrap().is_empty());
10575    }
10576
10577    #[test]
10578    fn test_agents_with_episodes_above_count_returns_high_activity_agents() {
10579        let store = EpisodicStore::new();
10580        let a1 = AgentId::new("agent-high");
10581        let a2 = AgentId::new("agent-low");
10582        store.add_episode(a1.clone(), "e1", 0.5).unwrap();
10583        store.add_episode(a1.clone(), "e2", 0.5).unwrap();
10584        store.add_episode(a1.clone(), "e3", 0.5).unwrap();
10585        store.add_episode(a2.clone(), "e1", 0.5).unwrap();
10586        let above = store.agents_with_episodes_above_count(2).unwrap();
10587        assert_eq!(above.len(), 1);
10588        assert_eq!(above[0], a1);
10589    }
10590
10591    #[test]
10592    fn test_agents_with_episodes_above_count_empty_when_none_qualify() {
10593        let store = EpisodicStore::new();
10594        let agent = AgentId::new("a");
10595        store.add_episode(agent.clone(), "e1", 0.5).unwrap();
10596        assert!(store.agents_with_episodes_above_count(5).unwrap().is_empty());
10597    }
10598
10599    // ── Round 56: episodes_matching_content, top_agent_by_importance, has_value_equal_to, value_contains ──
10600
10601    #[test]
10602    fn test_episodes_matching_content_returns_matching_episodes() {
10603        let store = EpisodicStore::new();
10604        let agent = AgentId::new("r56-emc-1");
10605        store.add_episode(agent.clone(), "user asked about Rust", 0.5).unwrap();
10606        store.add_episode(agent.clone(), "user asked about Python", 0.5).unwrap();
10607        let matched = store.episodes_matching_content(&agent, "Rust").unwrap();
10608        assert_eq!(matched.len(), 1);
10609        assert!(matched[0].content.contains("Rust"));
10610    }
10611
10612    #[test]
10613    fn test_episodes_matching_content_empty_for_no_match() {
10614        let store = EpisodicStore::new();
10615        let agent = AgentId::new("r56-emc-2");
10616        store.add_episode(agent.clone(), "something else", 0.5).unwrap();
10617        assert!(store.episodes_matching_content(&agent, "Rust").unwrap().is_empty());
10618    }
10619
10620    #[test]
10621    fn test_top_agent_by_importance_returns_highest_total_agent() {
10622        let store = EpisodicStore::new();
10623        let a1 = AgentId::new("r56-tabi-low");
10624        let a2 = AgentId::new("r56-tabi-high");
10625        store.add_episode(a1.clone(), "ep", 0.2).unwrap();
10626        store.add_episode(a2.clone(), "ep", 0.9).unwrap();
10627        let top = store.top_agent_by_importance().unwrap();
10628        assert_eq!(top.unwrap().as_str(), "r56-tabi-high");
10629    }
10630
10631    #[test]
10632    fn test_top_agent_by_importance_none_for_empty_store() {
10633        let store = EpisodicStore::new();
10634        assert!(store.top_agent_by_importance().unwrap().is_none());
10635    }
10636
10637    #[test]
10638    fn test_has_value_equal_to_true_when_match_exists() {
10639        let wm = WorkingMemory::new(10).unwrap();
10640        wm.set("key", "exact_value").unwrap();
10641        assert!(wm.has_value_equal_to("exact_value").unwrap());
10642    }
10643
10644    #[test]
10645    fn test_has_value_equal_to_false_when_no_match() {
10646        let wm = WorkingMemory::new(10).unwrap();
10647        wm.set("key", "other").unwrap();
10648        assert!(!wm.has_value_equal_to("exact_value").unwrap());
10649    }
10650
10651    #[test]
10652    fn test_value_contains_true_when_substr_in_value() {
10653        let wm = WorkingMemory::new(10).unwrap();
10654        wm.set("url", "https://example.com/path").unwrap();
10655        assert!(wm.value_contains("url", "example.com").unwrap());
10656    }
10657
10658    #[test]
10659    fn test_value_contains_false_when_key_absent() {
10660        let wm = WorkingMemory::new(10).unwrap();
10661        assert!(!wm.value_contains("missing", "anything").unwrap());
10662    }
10663
10664    // ── Round 50 ──────────────────────────────────────────────────────────────
10665
10666    #[test]
10667    fn test_avg_episode_importance_returns_mean() {
10668        let store = EpisodicStore::new();
10669        let agent = AgentId::new("r50-avg-1");
10670        store.add_episode(agent.clone(), "e1", 0.4).unwrap();
10671        store.add_episode(agent.clone(), "e2", 0.8).unwrap();
10672        let avg = store.avg_episode_importance(&agent).unwrap();
10673        assert!((avg - 0.6).abs() < 1e-6);
10674    }
10675
10676    #[test]
10677    fn test_avg_episode_importance_zero_for_unknown_agent() {
10678        let store = EpisodicStore::new();
10679        let agent = AgentId::new("r50-avg-unknown");
10680        assert_eq!(store.avg_episode_importance(&agent).unwrap(), 0.0);
10681    }
10682
10683    #[test]
10684    fn test_episode_content_bytes_total_correct() {
10685        let store = EpisodicStore::new();
10686        let agent = AgentId::new("r50-bytes-1");
10687        store.add_episode(agent.clone(), "hello", 0.5).unwrap();
10688        store.add_episode(agent.clone(), "world!", 0.5).unwrap();
10689        let total = store.episode_content_bytes_total(&agent).unwrap();
10690        assert_eq!(total, 11); // "hello" (5) + "world!" (6)
10691    }
10692
10693    #[test]
10694    fn test_episode_content_bytes_total_zero_for_unknown_agent() {
10695        let store = EpisodicStore::new();
10696        let agent = AgentId::new("r50-bytes-unknown");
10697        assert_eq!(store.episode_content_bytes_total(&agent).unwrap(), 0);
10698    }
10699
10700    // ── Round 57: episodes_after_timestamp, agent_with_min_importance_avg, keys_without_prefix, count_values_equal_to ──
10701
10702    #[test]
10703    fn test_episodes_after_timestamp_returns_newer_episodes() {
10704        use chrono::{Duration, Utc};
10705        let store = EpisodicStore::new();
10706        let agent = AgentId::new("r57-eat-1");
10707        store.add_episode(agent.clone(), "old", 0.5).unwrap();
10708        let cutoff = Utc::now();
10709        store.add_episode(agent.clone(), "new", 0.7).unwrap();
10710        let after = store.episodes_after_timestamp(&agent, cutoff).unwrap();
10711        assert_eq!(after.len(), 1);
10712        assert_eq!(after[0].content, "new");
10713        let _ = Duration::seconds(0); // suppress unused import warning
10714    }
10715
10716    #[test]
10717    fn test_agent_with_min_importance_avg_returns_lowest() {
10718        let store = EpisodicStore::new();
10719        let a1 = AgentId::new("r57-awmia-hi");
10720        let a2 = AgentId::new("r57-awmia-lo");
10721        store.add_episode(a1.clone(), "ep", 0.9).unwrap();
10722        store.add_episode(a2.clone(), "ep", 0.1).unwrap();
10723        let min_agent = store.agent_with_min_importance_avg().unwrap();
10724        assert_eq!(min_agent.unwrap().as_str(), "r57-awmia-lo");
10725    }
10726
10727    #[test]
10728    fn test_agent_with_min_importance_avg_none_for_empty_store() {
10729        let store = EpisodicStore::new();
10730        assert!(store.agent_with_min_importance_avg().unwrap().is_none());
10731    }
10732
10733    #[test]
10734    fn test_keys_without_prefix_returns_non_matching_keys() {
10735        let wm = WorkingMemory::new(10).unwrap();
10736        wm.set("cfg_host", "v").unwrap();
10737        wm.set("data_x", "v").unwrap();
10738        wm.set("data_y", "v").unwrap();
10739        let keys = wm.keys_without_prefix("cfg_").unwrap();
10740        assert_eq!(keys, vec!["data_x", "data_y"]);
10741    }
10742
10743    #[test]
10744    fn test_keys_without_prefix_empty_when_all_match() {
10745        let wm = WorkingMemory::new(10).unwrap();
10746        wm.set("cfg_a", "v").unwrap();
10747        assert!(wm.keys_without_prefix("cfg_").unwrap().is_empty());
10748    }
10749
10750    #[test]
10751    fn test_count_values_equal_to_returns_correct_count() {
10752        let wm = WorkingMemory::new(10).unwrap();
10753        wm.set("k1", "same").unwrap();
10754        wm.set("k2", "same").unwrap();
10755        wm.set("k3", "other").unwrap();
10756        assert_eq!(wm.count_values_equal_to("same").unwrap(), 2);
10757    }
10758
10759    #[test]
10760    fn test_count_values_equal_to_zero_for_no_match() {
10761        let wm = WorkingMemory::new(10).unwrap();
10762        wm.set("k", "v").unwrap();
10763        assert_eq!(wm.count_values_equal_to("missing").unwrap(), 0);
10764    }
10765
10766    // ── Round 57: has_episodes_for_agent, all_values_non_empty ────────────────
10767
10768    #[test]
10769    fn test_has_episodes_for_agent_true_after_adding_episode() {
10770        let store = EpisodicStore::new();
10771        let agent = AgentId::new("agent-r57");
10772        store.add_episode_with_tags(agent.clone(), "content", 0.5_f32, vec![]).unwrap();
10773        assert!(store.has_episodes_for_agent(&agent).unwrap());
10774    }
10775
10776    #[test]
10777    fn test_has_episodes_for_agent_false_for_unknown_agent() {
10778        let store = EpisodicStore::new();
10779        let agent = AgentId::new("nobody-r57");
10780        assert!(!store.has_episodes_for_agent(&agent).unwrap());
10781    }
10782
10783    #[test]
10784    fn test_all_values_non_empty_true_for_non_empty_values() {
10785        let wm = WorkingMemory::new(10).unwrap();
10786        wm.set("a", "hello").unwrap();
10787        wm.set("b", "world").unwrap();
10788        assert!(wm.all_values_non_empty().unwrap());
10789    }
10790
10791    #[test]
10792    fn test_all_values_non_empty_false_when_empty_value_present() {
10793        let wm = WorkingMemory::new(10).unwrap();
10794        wm.set("a", "hello").unwrap();
10795        wm.set("b", "").unwrap();
10796        assert!(!wm.all_values_non_empty().unwrap());
10797    }
10798
10799    #[test]
10800    fn test_all_values_non_empty_true_for_empty_store() {
10801        let wm = WorkingMemory::new(10).unwrap();
10802        assert!(wm.all_values_non_empty().unwrap());
10803    }
10804
10805    // ── Round 58: highest_importance_episode, avg_episode_content_words, has_duplicate_content, value_starts_with ──
10806
10807    #[test]
10808    fn test_highest_importance_episode_returns_max() {
10809        let store = EpisodicStore::new();
10810        let agent = AgentId::new("r58-hie-1");
10811        store.add_episode(agent.clone(), "low", 0.2).unwrap();
10812        store.add_episode(agent.clone(), "high", 0.9).unwrap();
10813        let top = store.highest_importance_episode(&agent).unwrap();
10814        assert_eq!(top.unwrap().importance, 0.9);
10815    }
10816
10817    #[test]
10818    fn test_highest_importance_episode_none_for_empty_agent() {
10819        let store = EpisodicStore::new();
10820        let agent = AgentId::new("r58-hie-unknown");
10821        assert!(store.highest_importance_episode(&agent).unwrap().is_none());
10822    }
10823
10824    #[test]
10825    fn test_avg_episode_content_words_returns_mean() {
10826        let store = EpisodicStore::new();
10827        let agent = AgentId::new("r58-aecw-1");
10828        store.add_episode(agent.clone(), "one two", 0.5).unwrap();
10829        store.add_episode(agent.clone(), "a b c d", 0.5).unwrap();
10830        let avg = store.avg_episode_content_words(&agent).unwrap();
10831        assert_eq!(avg, 3.0);
10832    }
10833
10834    #[test]
10835    fn test_has_duplicate_content_true_when_duplicates_exist() {
10836        let store = EpisodicStore::new();
10837        let agent = AgentId::new("r58-hdc-1");
10838        store.add_episode(agent.clone(), "same content", 0.5).unwrap();
10839        store.add_episode(agent.clone(), "same content", 0.7).unwrap();
10840        assert!(store.has_duplicate_content(&agent).unwrap());
10841    }
10842
10843    #[test]
10844    fn test_has_duplicate_content_false_when_all_unique() {
10845        let store = EpisodicStore::new();
10846        let agent = AgentId::new("r58-hdc-2");
10847        store.add_episode(agent.clone(), "first", 0.5).unwrap();
10848        store.add_episode(agent.clone(), "second", 0.5).unwrap();
10849        assert!(!store.has_duplicate_content(&agent).unwrap());
10850    }
10851
10852    #[test]
10853    fn test_value_starts_with_true_when_value_has_prefix() {
10854        let wm = WorkingMemory::new(10).unwrap();
10855        wm.set("url", "https://example.com").unwrap();
10856        assert!(wm.value_starts_with("url", "https://").unwrap());
10857    }
10858
10859    #[test]
10860    fn test_value_starts_with_false_when_key_absent() {
10861        let wm = WorkingMemory::new(10).unwrap();
10862        assert!(!wm.value_starts_with("missing", "https://").unwrap());
10863    }
10864
10865    // ── Round 51 ──────────────────────────────────────────────────────────────
10866
10867    #[test]
10868    fn test_value_with_max_bytes_returns_longest_value() {
10869        let wm = WorkingMemory::new(10).unwrap();
10870        wm.set("k1", "hi").unwrap();
10871        wm.set("k2", "hello world").unwrap();
10872        wm.set("k3", "ok").unwrap();
10873        assert_eq!(wm.value_with_max_bytes().unwrap(), Some("hello world".to_string()));
10874    }
10875
10876    #[test]
10877    fn test_value_with_max_bytes_none_for_empty_store() {
10878        let wm = WorkingMemory::new(10).unwrap();
10879        assert_eq!(wm.value_with_max_bytes().unwrap(), None);
10880    }
10881
10882    // ── Round 59: values_longer_than, has_key_with_prefix, min_episode_importance ──
10883
10884    #[test]
10885    fn test_values_longer_than_returns_matching_keys() {
10886        let wm = WorkingMemory::new(10).unwrap();
10887        wm.set("short", "hi").unwrap();
10888        wm.set("long", "hello world").unwrap();
10889        let keys = wm.values_longer_than(4).unwrap();
10890        assert_eq!(keys, vec!["long".to_string()]);
10891    }
10892
10893    #[test]
10894    fn test_values_longer_than_empty_when_none_qualify() {
10895        let wm = WorkingMemory::new(10).unwrap();
10896        wm.set("k", "ab").unwrap();
10897        let keys = wm.values_longer_than(100).unwrap();
10898        assert!(keys.is_empty());
10899    }
10900
10901    #[test]
10902    fn test_has_key_with_prefix_true() {
10903        let wm = WorkingMemory::new(10).unwrap();
10904        wm.set("agent:config", "val").unwrap();
10905        assert!(wm.has_key_with_prefix("agent:").unwrap());
10906    }
10907
10908    #[test]
10909    fn test_has_key_with_prefix_false() {
10910        let wm = WorkingMemory::new(10).unwrap();
10911        wm.set("other:config", "val").unwrap();
10912        assert!(!wm.has_key_with_prefix("agent:").unwrap());
10913    }
10914
10915    #[test]
10916    fn test_min_episode_importance_returns_minimum() {
10917        let store = EpisodicStore::new();
10918        let agent = AgentId::new("r59-min-imp");
10919        store.add_episode(agent.clone(), "first", 0.8).unwrap();
10920        store.add_episode(agent.clone(), "second", 0.2).unwrap();
10921        store.add_episode(agent.clone(), "third", 0.5).unwrap();
10922        let min = store.min_episode_importance(&agent).unwrap();
10923        assert!((min.unwrap() - 0.2_f32).abs() < 1e-6);
10924    }
10925
10926    #[test]
10927    fn test_min_episode_importance_none_for_unknown_agent() {
10928        let store = EpisodicStore::new();
10929        let agent = AgentId::new("r59-min-unknown");
10930        assert!(store.min_episode_importance(&agent).unwrap().is_none());
10931    }
10932
10933    // ── Round 60: value_count_matching_prefix, count_episodes_in_window ───────
10934
10935    #[test]
10936    fn test_value_count_matching_prefix_correct() {
10937        let wm = WorkingMemory::new(10).unwrap();
10938        wm.set("k1", "hello world").unwrap();
10939        wm.set("k2", "hello there").unwrap();
10940        wm.set("k3", "goodbye").unwrap();
10941        assert_eq!(wm.value_count_matching_prefix("hello").unwrap(), 2);
10942    }
10943
10944    #[test]
10945    fn test_value_count_matching_prefix_zero_when_none_match() {
10946        let wm = WorkingMemory::new(10).unwrap();
10947        wm.set("k1", "nope").unwrap();
10948        assert_eq!(wm.value_count_matching_prefix("yes").unwrap(), 0);
10949    }
10950
10951    #[test]
10952    fn test_count_episodes_in_window_correct() {
10953        use chrono::Utc;
10954        let store = EpisodicStore::new();
10955        let agent = AgentId::new("r60-window");
10956        store.add_episode(agent.clone(), "ep1", 0.5).unwrap();
10957        store.add_episode(agent.clone(), "ep2", 0.5).unwrap();
10958        // All episodes are created "now", so a wide window should include them
10959        let start = Utc::now() - chrono::Duration::hours(1);
10960        let end = Utc::now() + chrono::Duration::hours(1);
10961        let count = store.count_episodes_in_window(&agent, start, end).unwrap();
10962        assert_eq!(count, 2);
10963    }
10964
10965    #[test]
10966    fn test_count_episodes_in_window_zero_for_future_window() {
10967        use chrono::Utc;
10968        let store = EpisodicStore::new();
10969        let agent = AgentId::new("r60-future");
10970        store.add_episode(agent.clone(), "ep", 0.5).unwrap();
10971        let start = Utc::now() + chrono::Duration::hours(1);
10972        let end = Utc::now() + chrono::Duration::hours(2);
10973        let count = store.count_episodes_in_window(&agent, start, end).unwrap();
10974        assert_eq!(count, 0);
10975    }
10976
10977    // ── Round 61: total_tag_count, avg_tag_count_per_episode, keys_with_suffix, avg_key_length ──
10978
10979    #[test]
10980    fn test_total_tag_count_sums_tags_across_episodes() {
10981        let store = EpisodicStore::new();
10982        let agent = AgentId::new("r61-tags");
10983        store
10984            .add_episode_with_tags(agent.clone(), "ep1", 0.5, vec!["a".to_string(), "b".to_string()])
10985            .unwrap();
10986        store
10987            .add_episode_with_tags(agent.clone(), "ep2", 0.5, vec!["c".to_string()])
10988            .unwrap();
10989        assert_eq!(store.total_tag_count(&agent).unwrap(), 3);
10990    }
10991
10992    #[test]
10993    fn test_total_tag_count_zero_for_unknown_agent() {
10994        let store = EpisodicStore::new();
10995        let agent = AgentId::new("r61-tags-unknown");
10996        assert_eq!(store.total_tag_count(&agent).unwrap(), 0);
10997    }
10998
10999    #[test]
11000    fn test_avg_tag_count_per_episode_correct() {
11001        let store = EpisodicStore::new();
11002        let agent = AgentId::new("r61-avg-tags");
11003        store
11004            .add_episode_with_tags(agent.clone(), "e1", 0.5, vec!["x".to_string(), "y".to_string()])
11005            .unwrap();
11006        store
11007            .add_episode_with_tags(agent.clone(), "e2", 0.5, vec![])
11008            .unwrap();
11009        // 2 tags over 2 episodes = 1.0
11010        assert!((store.avg_tag_count_per_episode(&agent).unwrap() - 1.0).abs() < 1e-9);
11011    }
11012
11013    #[test]
11014    fn test_avg_tag_count_per_episode_zero_for_unknown_agent() {
11015        let store = EpisodicStore::new();
11016        let agent = AgentId::new("r61-avg-tags-unknown");
11017        assert_eq!(store.avg_tag_count_per_episode(&agent).unwrap(), 0.0);
11018    }
11019
11020    #[test]
11021    fn test_keys_with_suffix_returns_matching_keys() {
11022        let wm = WorkingMemory::new(10).unwrap();
11023        wm.set("config.json", "data").unwrap();
11024        wm.set("readme.md", "doc").unwrap();
11025        wm.set("schema.json", "meta").unwrap();
11026        let keys = wm.keys_with_suffix(".json").unwrap();
11027        assert_eq!(keys, vec!["config.json".to_string(), "schema.json".to_string()]);
11028    }
11029
11030    #[test]
11031    fn test_keys_with_suffix_empty_when_none_match() {
11032        let wm = WorkingMemory::new(10).unwrap();
11033        wm.set("hello", "world").unwrap();
11034        let keys = wm.keys_with_suffix(".json").unwrap();
11035        assert!(keys.is_empty());
11036    }
11037
11038    #[test]
11039    fn test_avg_key_length_correct() {
11040        let wm = WorkingMemory::new(10).unwrap();
11041        wm.set("ab", "v").unwrap();   // 2
11042        wm.set("abcd", "v").unwrap(); // 4
11043        let avg = wm.avg_key_length().unwrap();
11044        assert!((avg - 3.0).abs() < 1e-9);
11045    }
11046
11047    #[test]
11048    fn test_avg_key_length_zero_for_empty_store() {
11049        let wm = WorkingMemory::new(10).unwrap();
11050        assert_eq!(wm.avg_key_length().unwrap(), 0.0);
11051    }
11052
11053    // ── Round 62: episodes_above_importance_count, tag_union, entries_sorted_by_key_length ──
11054
11055    #[test]
11056    fn test_episodes_above_importance_count_correct() {
11057        let store = EpisodicStore::new();
11058        let agent = AgentId::new("r62-above");
11059        store.add_episode(agent.clone(), "e1", 0.8).unwrap();
11060        store.add_episode(agent.clone(), "e2", 0.3).unwrap();
11061        store.add_episode(agent.clone(), "e3", 0.6).unwrap();
11062        assert_eq!(store.episodes_above_importance_count(&agent, 0.5).unwrap(), 2);
11063    }
11064
11065    #[test]
11066    fn test_episodes_above_importance_count_zero_for_unknown_agent() {
11067        let store = EpisodicStore::new();
11068        let agent = AgentId::new("r62-above-unknown");
11069        assert_eq!(store.episodes_above_importance_count(&agent, 0.5).unwrap(), 0);
11070    }
11071
11072    #[test]
11073    fn test_tag_union_collects_all_unique_tags() {
11074        let store = EpisodicStore::new();
11075        let agent = AgentId::new("r62-union");
11076        store
11077            .add_episode_with_tags(agent.clone(), "e1", 0.5, vec!["a".to_string(), "b".to_string()])
11078            .unwrap();
11079        store
11080            .add_episode_with_tags(agent.clone(), "e2", 0.5, vec!["b".to_string(), "c".to_string()])
11081            .unwrap();
11082        let union = store.tag_union(&agent).unwrap();
11083        assert_eq!(union.len(), 3);
11084        assert!(union.contains("a"));
11085        assert!(union.contains("b"));
11086        assert!(union.contains("c"));
11087    }
11088
11089    #[test]
11090    fn test_tag_union_empty_for_unknown_agent() {
11091        let store = EpisodicStore::new();
11092        let agent = AgentId::new("r62-union-unknown");
11093        assert!(store.tag_union(&agent).unwrap().is_empty());
11094    }
11095
11096    #[test]
11097    fn test_entries_sorted_by_key_length_orders_by_length_then_alpha() {
11098        let wm = WorkingMemory::new(10).unwrap();
11099        wm.set("cc", "v").unwrap();
11100        wm.set("a", "v").unwrap();
11101        wm.set("bbb", "v").unwrap();
11102        let pairs = wm.entries_sorted_by_key_length().unwrap();
11103        let keys: Vec<&str> = pairs.iter().map(|(k, _)| k.as_str()).collect();
11104        assert_eq!(keys, vec!["a", "cc", "bbb"]);
11105    }
11106
11107    #[test]
11108    fn test_entries_sorted_by_key_length_empty_for_empty_store() {
11109        let wm = WorkingMemory::new(10).unwrap();
11110        assert!(wm.entries_sorted_by_key_length().unwrap().is_empty());
11111    }
11112
11113    // ── Round 63: episode_most_recent, episodes_by_importance_range, value_bytes_max ──
11114
11115    #[test]
11116    fn test_episode_most_recent_returns_newest() {
11117        use std::thread::sleep;
11118        use std::time::Duration;
11119        let store = EpisodicStore::new();
11120        let agent = AgentId::new("r63-recent");
11121        store.add_episode(agent.clone(), "first", 0.5).unwrap();
11122        sleep(Duration::from_millis(2));
11123        store.add_episode(agent.clone(), "latest", 0.8).unwrap();
11124        let recent = store.episode_most_recent(&agent).unwrap();
11125        assert_eq!(recent.map(|m| m.content), Some("latest".to_string()));
11126    }
11127
11128    #[test]
11129    fn test_episode_most_recent_none_for_unknown_agent() {
11130        let store = EpisodicStore::new();
11131        let agent = AgentId::new("r63-recent-unknown");
11132        assert!(store.episode_most_recent(&agent).unwrap().is_none());
11133    }
11134
11135    #[test]
11136    fn test_episodes_by_importance_range_correct() {
11137        let store = EpisodicStore::new();
11138        let agent = AgentId::new("r63-range");
11139        store.add_episode(agent.clone(), "low", 0.1).unwrap();
11140        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
11141        store.add_episode(agent.clone(), "high", 0.9).unwrap();
11142        let range = store.episodes_by_importance_range(&agent, 0.3, 0.7).unwrap();
11143        assert_eq!(range.len(), 1);
11144        assert_eq!(range[0].content, "mid");
11145    }
11146
11147    #[test]
11148    fn test_episodes_by_importance_range_empty_for_unknown_agent() {
11149        let store = EpisodicStore::new();
11150        let agent = AgentId::new("r63-range-unknown");
11151        assert!(store.episodes_by_importance_range(&agent, 0.0, 1.0).unwrap().is_empty());
11152    }
11153
11154    #[test]
11155    fn test_value_bytes_max_returns_max() {
11156        let wm = WorkingMemory::new(10).unwrap();
11157        wm.set("k1", "short").unwrap();
11158        wm.set("k2", "much longer value").unwrap();
11159        assert_eq!(wm.value_bytes_max().unwrap(), "much longer value".len());
11160    }
11161
11162    #[test]
11163    fn test_value_bytes_max_zero_for_empty_store() {
11164        let wm = WorkingMemory::new(10).unwrap();
11165        assert_eq!(wm.value_bytes_max().unwrap(), 0);
11166    }
11167
11168    #[test]
11169    fn test_total_content_words_returns_total_word_count() {
11170        let store = EpisodicStore::new();
11171        let agent = AgentId::new("r51-words-1");
11172        store.add_episode(agent.clone(), "hello world", 0.5).unwrap();
11173        store.add_episode(agent.clone(), "one two three", 0.5).unwrap();
11174        assert_eq!(store.total_content_words(&agent).unwrap(), 5);
11175    }
11176
11177    #[test]
11178    fn test_total_content_words_zero_for_unknown_agent() {
11179        let store = EpisodicStore::new();
11180        let agent = AgentId::new("r51-words-unknown");
11181        assert_eq!(store.total_content_words(&agent).unwrap(), 0);
11182    }
11183
11184    // ── Round 58: agent_count, max_key_length ─────────────────────────────────
11185
11186    #[test]
11187    fn test_agent_count_zero_for_empty_store() {
11188        let store = EpisodicStore::new();
11189        assert_eq!(store.agent_count().unwrap(), 0);
11190    }
11191
11192    #[test]
11193    fn test_agent_count_reflects_distinct_agents() {
11194        let store = EpisodicStore::new();
11195        let a = AgentId::new("r58-a");
11196        let b = AgentId::new("r58-b");
11197        store.add_episode_with_tags(a.clone(), "x", 0.5, vec![]).unwrap();
11198        store.add_episode_with_tags(a.clone(), "y", 0.5, vec![]).unwrap();
11199        store.add_episode_with_tags(b.clone(), "z", 0.5, vec![]).unwrap();
11200        assert_eq!(store.agent_count().unwrap(), 2);
11201    }
11202
11203    #[test]
11204    fn test_max_key_length_returns_longest() {
11205        let wm = WorkingMemory::new(10).unwrap();
11206        wm.set("short", "v").unwrap();
11207        wm.set("a-longer-key", "v").unwrap();
11208        assert_eq!(wm.max_key_length().unwrap(), "a-longer-key".len());
11209    }
11210
11211    #[test]
11212    fn test_max_key_length_zero_for_empty_store() {
11213        let wm = WorkingMemory::new(10).unwrap();
11214        assert_eq!(wm.max_key_length().unwrap(), 0);
11215    }
11216
11217    // ── Round 59: episodes_with_importance_above, value_char_count ─────────────
11218
11219    #[test]
11220    fn test_episodes_with_importance_above_filters_correctly() {
11221        let store = EpisodicStore::new();
11222        let agent = AgentId::new("r59-epia-agent");
11223        store.add_episode_with_tags(agent.clone(), "low", 0.2, vec![]).unwrap();
11224        store.add_episode_with_tags(agent.clone(), "high", 0.9, vec![]).unwrap();
11225        store.add_episode_with_tags(agent.clone(), "mid", 0.5, vec![]).unwrap();
11226        let result = store.episodes_with_importance_above(&agent, 0.4).unwrap();
11227        assert_eq!(result.len(), 2);
11228    }
11229
11230    #[test]
11231    fn test_episodes_with_importance_above_empty_for_unknown_agent() {
11232        let store = EpisodicStore::new();
11233        let agent = AgentId::new("r59-epia-unknown");
11234        let result = store.episodes_with_importance_above(&agent, 0.0).unwrap();
11235        assert!(result.is_empty());
11236    }
11237
11238    #[test]
11239    fn test_value_char_count_returns_correct_count() {
11240        let wm = WorkingMemory::new(10).unwrap();
11241        wm.set("greeting", "héllo").unwrap();
11242        assert_eq!(wm.value_char_count("greeting").unwrap(), 5);
11243    }
11244
11245    #[test]
11246    fn test_value_char_count_zero_for_missing_key() {
11247        let wm = WorkingMemory::new(10).unwrap();
11248        assert_eq!(wm.value_char_count("missing").unwrap(), 0);
11249    }
11250
11251    // ── Round 60: agent_episode_importance_sum, keys_longer_than ──────────────
11252
11253    #[test]
11254    fn test_agent_episode_importance_sum_correct() {
11255        let store = EpisodicStore::new();
11256        let agent = AgentId::new("r60-importance-sum");
11257        store.add_episode_with_tags(agent.clone(), "a", 0.4, vec![]).unwrap();
11258        store.add_episode_with_tags(agent.clone(), "b", 0.6, vec![]).unwrap();
11259        let sum = store.agent_episode_importance_sum(&agent).unwrap();
11260        assert!((sum - 1.0).abs() < 1e-6);
11261    }
11262
11263    #[test]
11264    fn test_agent_episode_importance_sum_zero_for_unknown_agent() {
11265        let store = EpisodicStore::new();
11266        let agent = AgentId::new("r60-importance-sum-unknown");
11267        assert_eq!(store.agent_episode_importance_sum(&agent).unwrap(), 0.0);
11268    }
11269
11270    #[test]
11271    fn test_keys_longer_than_filters_correctly() {
11272        let wm = WorkingMemory::new(10).unwrap();
11273        wm.set("ab", "v").unwrap();
11274        wm.set("abcdef", "v").unwrap();
11275        wm.set("xyz", "v").unwrap();
11276        let mut keys = wm.keys_longer_than(3).unwrap();
11277        keys.sort();
11278        assert_eq!(keys, vec!["abcdef".to_string()]);
11279    }
11280
11281    #[test]
11282    fn test_keys_longer_than_empty_when_none_qualify() {
11283        let wm = WorkingMemory::new(10).unwrap();
11284        wm.set("hi", "v").unwrap();
11285        assert!(wm.keys_longer_than(10).unwrap().is_empty());
11286    }
11287
11288    // ── Round 59: episodes_with_content_containing, average_value_length ──────
11289
11290    #[test]
11291    fn test_episodes_with_content_containing_returns_matches() {
11292        let store = EpisodicStore::new();
11293        let agent = AgentId::new("r59-content");
11294        store.add_episode_with_tags(agent.clone(), "hello world", 0.5, vec![]).unwrap();
11295        store.add_episode_with_tags(agent.clone(), "goodbye", 0.5, vec![]).unwrap();
11296        let result = store.episodes_with_content_containing(&agent, "hello").unwrap();
11297        assert_eq!(result.len(), 1);
11298        assert!(result[0].content.contains("hello"));
11299    }
11300
11301    #[test]
11302    fn test_episodes_with_content_containing_empty_for_no_match() {
11303        let store = EpisodicStore::new();
11304        let agent = AgentId::new("r59-content-miss");
11305        store.add_episode_with_tags(agent.clone(), "hello", 0.5, vec![]).unwrap();
11306        let result = store.episodes_with_content_containing(&agent, "xyz").unwrap();
11307        assert!(result.is_empty());
11308    }
11309
11310    #[test]
11311    fn test_average_value_length_computes_mean() {
11312        let wm = WorkingMemory::new(10).unwrap();
11313        wm.set("a", "hi").unwrap();      // 2 bytes
11314        wm.set("b", "hello").unwrap();   // 5 bytes
11315        // mean = 3.5
11316        let avg = wm.average_value_length().unwrap();
11317        assert!((avg - 3.5).abs() < 1e-9);
11318    }
11319
11320    #[test]
11321    fn test_average_value_length_zero_for_empty_store() {
11322        let wm = WorkingMemory::new(10).unwrap();
11323        assert_eq!(wm.average_value_length().unwrap(), 0.0);
11324    }
11325
11326    // ── Round 61: episodes_min_importance, episode_min_content_words, keys_shorter_than ──
11327
11328    #[test]
11329    fn test_episodes_min_importance_returns_minimum() {
11330        let store = EpisodicStore::new();
11331        let agent = AgentId::new("r61-min-imp");
11332        store.add_episode_with_tags(agent.clone(), "a", 0.8, vec![]).unwrap();
11333        store.add_episode_with_tags(agent.clone(), "b", 0.2, vec![]).unwrap();
11334        let min = store.episodes_min_importance(&agent).unwrap();
11335        assert!((min.unwrap() - 0.2_f32).abs() < 1e-6);
11336    }
11337
11338    #[test]
11339    fn test_episodes_min_importance_none_for_unknown_agent() {
11340        let store = EpisodicStore::new();
11341        let agent = AgentId::new("r61-min-imp-unknown");
11342        assert!(store.episodes_min_importance(&agent).unwrap().is_none());
11343    }
11344
11345    #[test]
11346    fn test_episode_min_content_words_returns_minimum() {
11347        let store = EpisodicStore::new();
11348        let agent = AgentId::new("r61-min-words");
11349        store.add_episode_with_tags(agent.clone(), "one two three", 0.5, vec![]).unwrap();
11350        store.add_episode_with_tags(agent.clone(), "hello", 0.5, vec![]).unwrap();
11351        assert_eq!(store.episode_min_content_words(&agent).unwrap(), 1);
11352    }
11353
11354    #[test]
11355    fn test_episode_min_content_words_zero_for_unknown_agent() {
11356        let store = EpisodicStore::new();
11357        let agent = AgentId::new("r61-min-words-unknown");
11358        assert_eq!(store.episode_min_content_words(&agent).unwrap(), 0);
11359    }
11360
11361    #[test]
11362    fn test_keys_shorter_than_filters_correctly() {
11363        let wm = WorkingMemory::new(10).unwrap();
11364        wm.set("a", "v").unwrap();
11365        wm.set("abcdef", "v").unwrap();
11366        wm.set("abc", "v").unwrap();
11367        let mut keys = wm.keys_shorter_than(3).unwrap();
11368        keys.sort();
11369        assert_eq!(keys, vec!["a".to_string()]);
11370    }
11371
11372    #[test]
11373    fn test_keys_shorter_than_empty_when_none_qualify() {
11374        let wm = WorkingMemory::new(10).unwrap();
11375        wm.set("long_key", "v").unwrap();
11376        assert!(wm.keys_shorter_than(1).unwrap().is_empty());
11377    }
11378
11379    // ── Round 62: episode_max_content_words, total_items, all_episodes, count_keys_below_bytes ──
11380
11381    #[test]
11382    fn test_episode_max_content_words_returns_maximum() {
11383        let store = EpisodicStore::new();
11384        let agent = AgentId::new("r62-max-words");
11385        store.add_episode_with_tags(agent.clone(), "one", 0.5, vec![]).unwrap();
11386        store.add_episode_with_tags(agent.clone(), "one two three four", 0.5, vec![]).unwrap();
11387        assert_eq!(store.episode_max_content_words(&agent).unwrap(), 4);
11388    }
11389
11390    #[test]
11391    fn test_episode_max_content_words_zero_for_unknown_agent() {
11392        let store = EpisodicStore::new();
11393        let agent = AgentId::new("r62-max-words-unknown");
11394        assert_eq!(store.episode_max_content_words(&agent).unwrap(), 0);
11395    }
11396
11397    #[test]
11398    fn test_total_items_counts_across_all_agents() {
11399        let store = EpisodicStore::new();
11400        let a = AgentId::new("r62-total-a");
11401        let b = AgentId::new("r62-total-b");
11402        store.add_episode_with_tags(a.clone(), "x", 0.5, vec![]).unwrap();
11403        store.add_episode_with_tags(a.clone(), "y", 0.5, vec![]).unwrap();
11404        store.add_episode_with_tags(b.clone(), "z", 0.5, vec![]).unwrap();
11405        assert_eq!(store.total_items().unwrap(), 3);
11406    }
11407
11408    #[test]
11409    fn test_all_episodes_returns_all_items() {
11410        let store = EpisodicStore::new();
11411        let a = AgentId::new("r62-all-a");
11412        let b = AgentId::new("r62-all-b");
11413        store.add_episode_with_tags(a.clone(), "ep1", 0.5, vec![]).unwrap();
11414        store.add_episode_with_tags(b.clone(), "ep2", 0.5, vec![]).unwrap();
11415        assert_eq!(store.all_episodes().unwrap().len(), 2);
11416    }
11417
11418    #[test]
11419    fn test_count_keys_below_bytes_correct() {
11420        let wm = WorkingMemory::new(10).unwrap();
11421        wm.set("a", "v").unwrap();
11422        wm.set("ab", "v").unwrap();
11423        wm.set("abcdefgh", "v").unwrap();
11424        assert_eq!(wm.count_keys_below_bytes(3).unwrap(), 2);
11425    }
11426
11427    #[test]
11428    fn test_count_keys_below_bytes_zero_when_none_qualify() {
11429        let wm = WorkingMemory::new(10).unwrap();
11430        wm.set("long_key", "v").unwrap();
11431        assert_eq!(wm.count_keys_below_bytes(1).unwrap(), 0);
11432    }
11433
11434    // ── Round 63: min_episode_count, values_with_prefix ──────────────────────
11435
11436    #[test]
11437    fn test_min_episode_count_returns_agent_with_fewest() {
11438        let store = EpisodicStore::new();
11439        let a = AgentId::new("r63-min-a");
11440        let b = AgentId::new("r63-min-b");
11441        store.add_episode_with_tags(a.clone(), "x", 0.5, vec![]).unwrap();
11442        store.add_episode_with_tags(a.clone(), "y", 0.5, vec![]).unwrap();
11443        store.add_episode_with_tags(b.clone(), "z", 0.5, vec![]).unwrap();
11444        let result = store.min_episode_count().unwrap().unwrap();
11445        assert_eq!(result.0, b);
11446        assert_eq!(result.1, 1);
11447    }
11448
11449    #[test]
11450    fn test_min_episode_count_none_for_empty_store() {
11451        let store = EpisodicStore::new();
11452        assert!(store.min_episode_count().unwrap().is_none());
11453    }
11454
11455    #[test]
11456    fn test_values_with_prefix_returns_matching_values() {
11457        let wm = WorkingMemory::new(10).unwrap();
11458        wm.set("k1", "prefix:hello").unwrap();
11459        wm.set("k2", "other:world").unwrap();
11460        wm.set("k3", "prefix:bye").unwrap();
11461        let mut vals = wm.values_with_prefix("prefix:").unwrap();
11462        vals.sort();
11463        assert_eq!(vals, vec!["prefix:bye".to_string(), "prefix:hello".to_string()]);
11464    }
11465
11466    #[test]
11467    fn test_values_with_prefix_empty_when_none_match() {
11468        let wm = WorkingMemory::new(10).unwrap();
11469        wm.set("k1", "hello").unwrap();
11470        assert!(wm.values_with_prefix("xyz:").unwrap().is_empty());
11471    }
11472
11473    // ── Round 63: max_episode_importance ─────────────────────────────────────
11474
11475    #[test]
11476    fn test_max_episode_importance_returns_maximum() {
11477        let store = EpisodicStore::new();
11478        let agent = AgentId::new("r63-max-imp");
11479        store.add_episode_with_tags(agent.clone(), "a", 0.3, vec![]).unwrap();
11480        store.add_episode_with_tags(agent.clone(), "b", 0.9, vec![]).unwrap();
11481        store.add_episode_with_tags(agent.clone(), "c", 0.6, vec![]).unwrap();
11482        let max = store.max_episode_importance(&agent).unwrap();
11483        assert!((max.unwrap() - 0.9_f32).abs() < 1e-6);
11484    }
11485
11486    #[test]
11487    fn test_max_episode_importance_none_for_unknown_agent() {
11488        let store = EpisodicStore::new();
11489        let agent = AgentId::new("r63-max-imp-unknown");
11490        assert!(store.max_episode_importance(&agent).unwrap().is_none());
11491    }
11492
11493    #[test]
11494    fn test_max_episode_importance_single_episode() {
11495        let store = EpisodicStore::new();
11496        let agent = AgentId::new("r63-max-imp-single");
11497        store.add_episode_with_tags(agent.clone(), "only", 0.42, vec![]).unwrap();
11498        let max = store.max_episode_importance(&agent).unwrap();
11499        assert!((max.unwrap() - 0.42_f32).abs() < 1e-6);
11500    }
11501
11502    // ── Round 64: episodes_in_range, agent_importance_range, values_with_suffix ──
11503
11504    #[test]
11505    fn test_episodes_in_range_returns_filtered_episodes() {
11506        let store = EpisodicStore::new();
11507        let agent = AgentId::new("r64-ep-range");
11508        store.add_episode_with_tags(agent.clone(), "low", 0.1, vec![]).unwrap();
11509        store.add_episode_with_tags(agent.clone(), "mid", 0.5, vec![]).unwrap();
11510        store.add_episode_with_tags(agent.clone(), "high", 0.9, vec![]).unwrap();
11511        let results = store.episodes_in_range(&agent, 0.3, 0.7).unwrap();
11512        assert_eq!(results.len(), 1);
11513        assert_eq!(results[0].content, "mid");
11514    }
11515
11516    #[test]
11517    fn test_episodes_in_range_empty_for_unknown_agent() {
11518        let store = EpisodicStore::new();
11519        let agent = AgentId::new("r64-ep-range-unknown");
11520        assert!(store.episodes_in_range(&agent, 0.0, 1.0).unwrap().is_empty());
11521    }
11522
11523    #[test]
11524    fn test_agent_importance_range_returns_min_max() {
11525        let store = EpisodicStore::new();
11526        let agent = AgentId::new("r64-imp-range");
11527        store.add_episode_with_tags(agent.clone(), "a", 0.2, vec![]).unwrap();
11528        store.add_episode_with_tags(agent.clone(), "b", 0.8, vec![]).unwrap();
11529        let range = store.agent_importance_range(&agent).unwrap();
11530        let (min, max) = range.unwrap();
11531        assert!((min - 0.2_f32).abs() < 1e-6);
11532        assert!((max - 0.8_f32).abs() < 1e-6);
11533    }
11534
11535    #[test]
11536    fn test_agent_importance_range_none_for_unknown_agent() {
11537        let store = EpisodicStore::new();
11538        let agent = AgentId::new("r64-imp-range-unknown");
11539        assert!(store.agent_importance_range(&agent).unwrap().is_none());
11540    }
11541
11542    #[test]
11543    fn test_values_with_suffix_returns_matching() {
11544        let wm = WorkingMemory::new(10).unwrap();
11545        wm.set("k1", "hello world").unwrap();
11546        wm.set("k2", "foo world").unwrap();
11547        wm.set("k3", "bar xyz").unwrap();
11548        let mut vals = wm.values_with_suffix("world").unwrap();
11549        vals.sort();
11550        assert_eq!(vals, vec!["foo world", "hello world"]);
11551    }
11552
11553    #[test]
11554    fn test_values_with_suffix_empty_when_none_match() {
11555        let wm = WorkingMemory::new(10).unwrap();
11556        wm.set("k1", "hello").unwrap();
11557        assert!(wm.values_with_suffix("xyz").unwrap().is_empty());
11558    }
11559}