use super::Episodes;
use crate::db;
use crate::error::{AlayaError, Result};
use crate::types::*;
use crate::{graph, store};
impl Episodes<'_> {
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
pub fn store(&self, episode: &NewEpisode) -> Result<EpisodeId> {
if episode.content.trim().is_empty() {
return Err(AlayaError::InvalidInput(
"episode content must not be empty".into(),
));
}
if episode.session_id.trim().is_empty() {
return Err(AlayaError::InvalidInput(
"session_id must not be empty".into(),
));
}
const MAX_CONTENT_BYTES: usize = 100 * 1024; if episode.content.len() > MAX_CONTENT_BYTES {
return Err(AlayaError::InvalidInput(format!(
"episode content exceeds 100KB limit ({} bytes)",
episode.content.len()
)));
}
if let Some(ref emb) = episode.embedding {
if emb.iter().any(|v| !v.is_finite()) {
return Err(AlayaError::InvalidInput(
"embedding contains NaN or infinity values".into(),
));
}
}
let id = db::transact(self.conn, |tx| {
let id = store::episodic::store_episode(tx, episode)?;
let effective_embedding = match &episode.embedding {
Some(emb) => Some(emb.clone()),
None => self
.embedding_provider
.and_then(|p| p.embed(&episode.content).ok()),
};
if let Some(ref emb) = effective_embedding {
store::embeddings::store_embedding(tx, "episode", id.0, emb, "")?;
}
store::strengths::init_strength(tx, NodeRef::Episode(id))?;
if let Some(prev) = episode.context.preceding_episode {
graph::links::create_link(
tx,
NodeRef::Episode(prev),
NodeRef::Episode(id),
LinkType::Temporal,
0.5,
)?;
}
Ok(id)
})?;
if let Some(h) = self.hooks {
h.on_episode_stored(id);
}
Ok(id)
}
pub fn by_session(&self, session_id: &str) -> Result<Vec<Episode>> {
store::episodic::get_episodes_by_session(self.conn, session_id)
}
pub fn unconsolidated(&self, limit: u32) -> Result<Vec<Episode>> {
store::episodic::get_unconsolidated_episodes(self.conn, limit)
}
}
#[cfg(test)]
mod tests {
use crate::testutil::fixtures::*;
use crate::Alaya;
#[test]
fn store_and_retrieve_episode() {
let alaya = Alaya::open_in_memory().unwrap();
let id = alaya.episodes().store(&episode("test content")).unwrap();
assert!(id.0 > 0);
let eps = alaya.episodes().by_session("test-session").unwrap();
assert_eq!(eps.len(), 1);
assert_eq!(eps[0].content, "test content");
}
#[test]
fn store_rejects_empty_content() {
let alaya = Alaya::open_in_memory().unwrap();
let result = alaya.episodes().store(&episode(""));
assert!(result.is_err());
}
#[test]
fn store_rejects_empty_session_id() {
let alaya = Alaya::open_in_memory().unwrap();
let mut ep = episode("content");
ep.session_id = "".to_string();
assert!(alaya.episodes().store(&ep).is_err());
}
#[test]
fn store_rejects_oversized_content() {
let alaya = Alaya::open_in_memory().unwrap();
let mut ep = episode("x");
ep.content = "x".repeat(100 * 1024 + 1);
let result = alaya.episodes().store(&ep);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("100KB"), "error should mention limit: {err}");
}
#[test]
fn store_rejects_nan_embedding() {
let alaya = Alaya::open_in_memory().unwrap();
let mut ep = episode("valid content");
ep.embedding = Some(vec![1.0, f32::NAN, 3.0]);
let result = alaya.episodes().store(&ep);
assert!(result.is_err());
}
#[test]
fn store_rejects_infinity_embedding() {
let alaya = Alaya::open_in_memory().unwrap();
let mut ep = episode("valid content");
ep.embedding = Some(vec![1.0, f32::INFINITY, 3.0]);
let result = alaya.episodes().store(&ep);
assert!(result.is_err());
}
#[test]
fn store_accepts_valid_embedding() {
let alaya = Alaya::open_in_memory().unwrap();
let mut ep = episode("valid content");
ep.embedding = Some(vec![0.1, 0.2, 0.3]);
let result = alaya.episodes().store(&ep);
assert!(result.is_ok());
}
#[test]
fn unconsolidated_returns_new_episodes() {
let alaya = Alaya::open_in_memory().unwrap();
alaya.episodes().store(&episode("msg 1")).unwrap();
alaya.episodes().store(&episode("msg 2")).unwrap();
let uncons = alaya.episodes().unconsolidated(100).unwrap();
assert_eq!(uncons.len(), 2);
}
}