use crate::error::Error;
use crate::project_drain::{
self, Entry, EntryId, LIST_CAP, MAX_PASSES, ProjectEntries, partial_delete_error,
};
use crate::store::{SearchResult, SessionMeta, Store, parse_meta};
use crate::util;
#[cfg(feature = "vipune-store")]
pub struct VipuneStore {
store: vipune::MemoryStore,
}
#[cfg(feature = "vipune-store")]
impl VipuneStore {
pub fn open() -> Result<Self, Error> {
let config = vipune::Config::load().map_err(|e| Error::Store(e.to_string()))?;
let database_path = config.database_path.clone();
let embedding_model = config.embedding_model.clone();
let store = vipune::MemoryStore::new(&database_path, &embedding_model, config)
.map_err(|e| Error::Store(e.to_string()))?;
Ok(Self { store })
}
}
#[cfg(feature = "vipune-store")]
impl Store for VipuneStore {
fn index(
&mut self,
project_id: &str,
content: &str,
meta: &SessionMeta,
) -> Result<String, Error> {
let meta_json = serde_json::to_string(meta).map_err(|e| Error::Store(e.to_string()))?;
match self
.store
.add_with_conflict(project_id, content, Some(&meta_json), true)
{
Ok(vipune::AddResult::Added { id }) => Ok(id),
Ok(vipune::AddResult::Conflicts { .. }) => Ok(String::new()),
Err(e) => Err(Error::Store(e.to_string())),
}
}
fn search(
&mut self,
project_id: &str,
query: &str,
limit: usize,
) -> Result<Vec<SearchResult>, Error> {
let memories = self
.store
.search_hybrid(project_id, query, limit, 0.3)
.map_err(|e| Error::Store(e.to_string()))?;
Ok(memories
.into_iter()
.map(|m| SearchResult {
id: m.id,
meta: m.metadata.as_deref().and_then(parse_meta),
content: m.content,
similarity: m.similarity,
snippet: None,
})
.collect())
}
fn delete_project(&mut self, project_id: &str) -> Result<usize, Error> {
let drain = ProjectDrain {
store: &self.store,
project_id,
};
project_drain::drain_project(&drain, LIST_CAP, MAX_PASSES)
}
fn cleanup_stale(&mut self, project_id: &str, max_age_secs: i64) -> Result<usize, Error> {
let cutoff = project_drain::stale_cutoff(util::now_epoch(), max_age_secs);
let drain = ProjectDrain {
store: &self.store,
project_id,
};
let out = project_drain::cleanup_stale_in(&drain, cutoff, LIST_CAP)?;
if out.window_capped {
project_drain::warn_window_capped_once(&mut std::io::stderr(), &WINDOW_WARNED);
}
if out.failed > 0 {
return Err(partial_delete_error(out.deleted, out.failed));
}
Ok(out.deleted)
}
}
static WINDOW_WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
struct ProjectDrain<'a> {
store: &'a vipune::MemoryStore,
project_id: &'a str,
}
impl ProjectEntries for ProjectDrain<'_> {
fn list(&self, limit: usize) -> Result<Vec<Entry>, Error> {
let memories = self
.store
.list(self.project_id, limit)
.map_err(|e| Error::Store(e.to_string()))?;
Ok(memories
.into_iter()
.map(|m| {
let meta = m.metadata.as_deref().and_then(parse_meta);
let is_oo_entry = meta.as_ref().is_some_and(|meta| meta.source == "oo");
Entry {
id: m.id,
timestamp: meta.map_or(0, |meta| meta.timestamp),
is_oo_entry,
}
})
.collect())
}
fn list_ids(&self, limit: usize) -> Result<Vec<EntryId>, Error> {
let memories = self
.store
.list(self.project_id, limit)
.map_err(|e| Error::Store(e.to_string()))?;
Ok(memories.into_iter().map(|m| m.id).collect())
}
fn delete(&self, id: &str) -> Result<bool, Error> {
self.store
.delete(id)
.map_err(|e| Error::Store(e.to_string()))
}
}