use foundation_core::valtron::Stream;
use foundation_db::traits::{DocumentStore, PromotableDocument};
use foundation_db::StorageResult;
use super::memory_store::{MemoryStore, MemoryTier, SessionMemory};
use crate::types::{SessionId, SessionRecord};
impl PromotableDocument for SessionRecord {
fn record_type(&self) -> Option<String> {
Some(
match self {
SessionRecord::Conversation { .. } => "conversation",
SessionRecord::WorkingMemory { .. } => "working_memory",
SessionRecord::Observation { .. } => "observation",
SessionRecord::Reflection { .. } => "reflection",
SessionRecord::FailedAction { .. } => "failed_action",
SessionRecord::Summary { .. } => "summary",
SessionRecord::Retracted { .. } => "retracted",
}
.to_string(),
)
}
fn summary(&self) -> Option<String> {
match self {
SessionRecord::WorkingMemory { facts, .. } => facts.first().map(|f| f.fact.clone()),
SessionRecord::Observation { observations, .. } => {
observations.first().map(|o| o.content.clone())
}
SessionRecord::Reflection { reflections, .. } => {
reflections.first().map(|r| r.summary.clone())
}
_ => None,
}
}
}
fn tier_ref(memory: &SessionMemory, tier: MemoryTier) -> Option<&SessionRecord> {
match tier {
MemoryTier::Working => memory.working.as_ref(),
MemoryTier::Observation => memory.observation.as_ref(),
MemoryTier::Reflection => memory.reflection.as_ref(),
}
}
pub struct MemoryCoordinator<M, D> {
memory: M,
audit: D,
}
impl<M: MemoryStore, D: DocumentStore> MemoryCoordinator<M, D> {
pub fn new(memory: M, audit: D) -> Self {
Self { memory, audit }
}
fn audit_key(session: &SessionId) -> String {
format!("session:{session}")
}
pub async fn record_async(
&self,
session: &SessionId,
record: &SessionRecord,
) -> StorageResult<()> {
self.audit
.append_promotable(&Self::audit_key(session), record.clone())?;
if let Err(e) = self.memory.set_async(session, record).await {
tracing::warn!(
session = %session,
error = %e,
"memory cache write failed after audit succeeded; cache will rebuild on hydrate fallback"
);
}
Ok(())
}
pub async fn hydrate_async(&self, session: &SessionId) -> StorageResult<SessionMemory> {
let mut memory = self.memory.hydrate_async(session).await?;
let missing: Vec<MemoryTier> = [
MemoryTier::Working,
MemoryTier::Observation,
MemoryTier::Reflection,
]
.into_iter()
.filter(|t| tier_ref(&memory, *t).is_none())
.collect();
if missing.is_empty() {
return Ok(memory);
}
for item in self
.audit
.scan_all::<SessionRecord>(&Self::audit_key(session))?
{
let record = match item {
Stream::Next(Ok(r)) => r,
Stream::Next(Err(e)) => return Err(e),
_ => continue,
};
if let Some(tier) = MemoryTier::of(&record) {
if missing.contains(&tier) {
*slot_mut(&mut memory, tier) = Some(record); }
}
}
for tier in missing {
if let Some(record) = tier_ref(&memory, tier).cloned() {
self.memory.set_async(session, &record).await?;
}
}
Ok(memory)
}
pub fn memory(&self) -> &M {
&self.memory
}
}
fn slot_mut(memory: &mut SessionMemory, tier: MemoryTier) -> &mut Option<SessionRecord> {
match tier {
MemoryTier::Working => &mut memory.working,
MemoryTier::Observation => &mut memory.observation,
MemoryTier::Reflection => &mut memory.reflection,
}
}