use super::helpers::{
build_closet_index, is_low_quality_content, merge_into, rebuild_index_from_drawers,
};
use crate::memory_core::decay::DecayConfig;
use crate::memory_core::embed::Embedder;
use crate::memory_core::maintenance_log::DeletionReason;
use crate::memory_core::palace::Drawer;
use crate::memory_core::retrieval::{PalaceHandle, shared_embedder};
use crate::memory_core::store::vector::VectorStore;
use crate::memory_core::timeouts;
use anyhow::Result;
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use uuid::Uuid;
pub(super) const DREAM_EMBED_CHUNK: usize = 16;
pub(super) async fn content_prune_pass(
handle: &Arc<PalaceHandle>,
started: std::time::Instant,
budget: Duration,
min_words: usize,
) -> Result<usize> {
let victims: Vec<Uuid> = {
let drawers = handle.drawers.read();
let mut victims: Vec<Uuid> = Vec::new();
for drawer in drawers.iter() {
if started.elapsed() >= budget {
break;
}
if drawer.drawer_type.is_protected() {
continue;
}
if is_low_quality_content(drawer.content(), min_words) {
victims.push(drawer.id);
}
}
victims
};
let mut count = 0usize;
for id in victims {
if started.elapsed() >= budget {
break;
}
match handle
.forget_for_maintenance(id, DeletionReason::DreamContentPrune, None)
.await
{
Ok(outcome) if outcome.is_deleted() => count += 1,
Ok(_) => {}
Err(e) => tracing::warn!(?id, "dream content prune: forget failed: {e:#}"),
}
}
Ok(count)
}
pub(super) async fn compact_pass(
handle: &Arc<PalaceHandle>,
started: std::time::Instant,
budget: Duration,
) -> Result<usize> {
let mut removed: usize = 0;
let (drawer_count, index_size_after) = {
let _write_guard = timeouts::lock_with_timeout(
&handle.write_mutex,
timeouts::write_lock_timeout(),
handle.id.as_str(),
)
.await?;
let drawer_ids: HashSet<Uuid> = handle.drawers.read().iter().map(|d| d.id).collect();
let vector_ids = handle.vector_store.all_ids();
for vid in vector_ids {
if started.elapsed() >= budget {
break;
}
if drawer_ids.contains(&vid) {
continue;
}
match handle.vector_store.remove(vid).await {
Ok(()) => removed += 1,
Err(e) => tracing::warn!(?vid, "dream compact: vector remove failed: {e:#}"),
}
}
(drawer_ids.len(), handle.vector_store.index_size())
};
if drawer_count > 0 && index_size_after > drawer_count.saturating_mul(2) + 1 {
let rebuilt = rebuild_index_from_drawers(handle, started, budget)
.await
.map_err(|e| e.context("dream compact rebuild"))?;
let delta = index_size_after.saturating_sub(rebuilt);
removed = removed.saturating_add(delta);
}
Ok(removed)
}
pub(super) async fn dedup_pass(
handle: &Arc<PalaceHandle>,
started: std::time::Instant,
budget: Duration,
dedup_threshold: f32,
) -> Result<usize> {
let embedder = shared_embedder()
.await
.map_err(|e| e.context("acquire shared embedder for dream dedup"))?;
dedup_pass_with_embedder(
handle,
started,
budget,
dedup_threshold,
embedder.as_ref(),
timeouts::embed_batch_timeout(),
)
.await
}
pub(super) async fn dedup_pass_with_embedder(
handle: &Arc<PalaceHandle>,
started: std::time::Instant,
budget: Duration,
dedup_threshold: f32,
embedder: &(dyn Embedder + Send + Sync),
embed_timeout: Duration,
) -> Result<usize> {
let snapshot: Vec<Drawer> = handle.drawers.read().clone();
if snapshot.len() < 2 {
return Ok(0);
}
let mut merges: usize = 0;
let mut already_removed: HashSet<Uuid> = HashSet::new();
for chunk in snapshot.chunks(DREAM_EMBED_CHUNK) {
if started.elapsed() >= budget {
break;
}
let contents: Vec<String> = chunk.iter().map(|d| d.content().to_string()).collect();
let vectors = tokio::time::timeout(embed_timeout, embedder.embed_batch(&contents))
.await
.map_err(|_| {
anyhow::anyhow!(
"embed_batch of {} drawers timed out after {embed_timeout:?} \
during dream dedup",
contents.len()
)
})?
.map_err(|e| e.context("batch embed drawers for dream dedup"))?;
drop(contents);
if vectors.len() != chunk.len() {
anyhow::bail!(
"embedder returned {} vectors for {} drawers",
vectors.len(),
chunk.len()
);
}
for (drawer, query_vec) in chunk.iter().zip(vectors.iter()) {
if started.elapsed() >= budget {
break;
}
if already_removed.contains(&drawer.id) {
continue;
}
if drawer.drawer_type.is_protected() {
continue;
}
merges += dedup_one(
handle,
&snapshot,
drawer,
query_vec,
dedup_threshold,
&mut already_removed,
)
.await?;
}
}
Ok(merges)
}
async fn dedup_one(
handle: &Arc<PalaceHandle>,
snapshot: &[Drawer],
drawer: &Drawer,
query_vec: &[f32],
dedup_threshold: f32,
already_removed: &mut HashSet<Uuid>,
) -> Result<usize> {
let hits = handle.vector_store.search(query_vec, 3).await?;
for hit in hits.into_iter() {
if hit.drawer_id == drawer.id || already_removed.contains(&hit.drawer_id) {
continue;
}
if hit.score < dedup_threshold {
continue;
}
let Some(hit_drawer) = snapshot.iter().find(|d| d.id == hit.drawer_id) else {
continue;
};
if hit_drawer.drawer_type.is_protected() {
continue;
}
let (survivor, loser) = if drawer.importance >= hit_drawer.importance {
(drawer, hit_drawer)
} else {
(hit_drawer, drawer)
};
merge_into(handle, survivor, loser);
let survivor_ref = Some((survivor.id, Some(hit.score)));
if let Err(e) = handle
.forget_for_maintenance(loser.id, DeletionReason::DreamDedup, survivor_ref)
.await
{
tracing::warn!(id = ?loser.id, "dream dedup: loser evict failed: {e:#}");
}
already_removed.insert(loser.id);
return Ok(1);
}
Ok(0)
}
pub(super) async fn prune_pass(
handle: &Arc<PalaceHandle>,
started: std::time::Instant,
budget: Duration,
prune_importance: f32,
) -> Result<usize> {
const MIN_AGE_DAYS: f32 = 30.0;
let victims: Vec<Uuid> = {
let drawers = handle.drawers.read();
let mut victims: Vec<Uuid> = Vec::new();
for drawer in drawers.iter() {
if started.elapsed() >= budget {
break;
}
if drawer.drawer_type.is_protected() {
continue;
}
let age = DecayConfig::age_days(drawer.created_at);
let boost = drawer.accumulated_boost(&handle.decay_config);
let eff = handle
.decay_config
.effective_importance(drawer.importance, age, boost);
if eff <= prune_importance && age > MIN_AGE_DAYS {
victims.push(drawer.id);
}
}
victims
};
let mut count = 0usize;
for id in victims {
match handle
.forget_for_maintenance(id, DeletionReason::DreamPrune, None)
.await
{
Ok(outcome) if outcome.is_deleted() => count += 1,
Ok(_) => {}
Err(e) => tracing::warn!(?id, "dream prune: forget failed: {e:#}"),
}
}
Ok(count)
}
pub(super) fn refresh_closets(handle: &Arc<PalaceHandle>) -> usize {
let new_index = {
let drawers = handle.drawers.read();
build_closet_index(&drawers)
};
let count = new_index.len();
let mut closets = handle.closets.write();
*closets = new_index;
count
}