use std::time::Duration;
use rmcp::ServiceExt;
use velesdb_memory::mcp::McpServer;
use velesdb_memory::{DynEmbedder, HashEmbedder, MemoryService, NativeStore, DEFAULT_DIMENSION};
fn main() -> Result<(), Box<dyn std::error::Error>> {
if std::env::args()
.nth(1)
.is_some_and(|arg| arg == "--version" || arg == "-V")
{
println!("velesdb-memory {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
#[cfg(unix)]
let original_parent = std::os::unix::process::parent_id();
#[cfg(not(unix))]
let original_parent = 0_u32;
let store_path = std::env::var("VELESDB_MEMORY_PATH").unwrap_or_else(|_| default_store_path());
let embedder = build_embedder()?;
let service = open_store_with_actionable_lock_error(&store_path, embedder)?;
let server = apply_ingest_roots(apply_default_ttl(build_server(service)?)?)?;
tokio::runtime::Runtime::new()?.block_on(async move {
spawn_orphan_watchdog(original_parent);
let running = server
.serve((tokio::io::stdin(), tokio::io::stdout()))
.await?;
running.waiting().await?;
Ok::<(), Box<dyn std::error::Error>>(())
})
}
#[cfg(unix)]
const ORPHAN_CHECK_INTERVAL: std::time::Duration = std::time::Duration::from_secs(2);
#[cfg(unix)]
fn spawn_orphan_watchdog(original_parent: u32) {
use std::os::unix::process::parent_id;
tokio::spawn(async move {
loop {
tokio::time::sleep(ORPHAN_CHECK_INTERVAL).await;
let current_parent = parent_id();
if current_parent != original_parent {
eprintln!(
"[velesdb-memory] parent process (pid {original_parent}) is gone \
(now reparented under pid {current_parent}) — exiting to release \
the store lock rather than leak a zombie session (#1448)"
);
std::process::exit(0);
}
}
});
}
#[cfg(not(unix))]
fn spawn_orphan_watchdog(_original_parent: u32) {}
const LOCK_RETRY_ATTEMPTS: u32 = 3;
const LOCK_RETRY_DELAY: Duration = Duration::from_millis(500);
fn open_store_with_actionable_lock_error(
store_path: &str,
embedder: DynEmbedder,
) -> Result<MemoryService<DynEmbedder>, Box<dyn std::error::Error>> {
use velesdb_memory::MemoryError;
let dimension = embedder.dimension();
let mut last_locked_path: Option<String> = None;
for attempt in 0..LOCK_RETRY_ATTEMPTS {
match NativeStore::open(store_path, dimension) {
Ok(store) => return Ok(MemoryService::with_store(store, embedder)),
Err(MemoryError::Storage(velesdb_core::Error::DatabaseLocked(locked_path))) => {
last_locked_path = Some(locked_path);
if attempt + 1 < LOCK_RETRY_ATTEMPTS {
std::thread::sleep(LOCK_RETRY_DELAY);
}
}
Err(other) => return Err(other.into()),
}
}
let locked_path = last_locked_path.unwrap_or_else(|| store_path.to_owned());
eprintln!(
"[velesdb-memory] another velesdb-memory process holds {locked_path} — \
kill it (pkill velesdb-memory) or point VELESDB_MEMORY_PATH elsewhere"
);
std::process::exit(1);
}
fn default_store_path() -> String {
let home = std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.filter(|h| !h.is_empty());
match home {
Some(home) => std::path::Path::new(&home)
.join(".velesdb-memory")
.to_string_lossy()
.into_owned(),
None => "./velesdb-memory-store".to_owned(),
}
}
fn apply_default_ttl(server: McpServer) -> Result<McpServer, Box<dyn std::error::Error>> {
match std::env::var("VELESDB_MEMORY_DEFAULT_TTL") {
Ok(raw) => {
let ttl_seconds: u64 = raw.trim().parse().map_err(|_| {
format!(
"VELESDB_MEMORY_DEFAULT_TTL must be a non-negative integer (seconds), got '{raw}'"
)
})?;
Ok(server.with_default_ttl(ttl_seconds))
}
Err(_) => Ok(server),
}
}
#[cfg(feature = "context")]
fn apply_ingest_roots(server: McpServer) -> Result<McpServer, Box<dyn std::error::Error>> {
match std::env::var("VELESDB_MEMORY_INGEST_ROOTS") {
Ok(raw) if !raw.trim().is_empty() => {
let roots = velesdb_memory::context::IngestRoots::parse(&raw)?;
Ok(server.with_ingest_roots(roots))
}
_ => Ok(server),
}
}
#[cfg(not(feature = "context"))]
#[allow(clippy::unnecessary_wraps)]
fn apply_ingest_roots(server: McpServer) -> Result<McpServer, Box<dyn std::error::Error>> {
Ok(server)
}
#[cfg(feature = "extract")]
fn build_server(
service: MemoryService<DynEmbedder>,
) -> Result<McpServer, Box<dyn std::error::Error>> {
let server = McpServer::new(service);
match std::env::var("VELESDB_MEMORY_EXTRACTOR").as_deref() {
Ok("ollama") => Ok(server.with_extractor(build_ollama_extractor()?)),
Ok("none") | Err(_) => Ok(server),
Ok(other) => {
Err(format!("unknown VELESDB_MEMORY_EXTRACTOR '{other}' (expected 'ollama')").into())
}
}
}
#[cfg(not(feature = "extract"))]
#[allow(clippy::unnecessary_wraps)]
fn build_server(
service: MemoryService<DynEmbedder>,
) -> Result<McpServer, Box<dyn std::error::Error>> {
Ok(McpServer::new(service))
}
#[cfg(feature = "extract")]
fn build_ollama_extractor() -> Result<velesdb_memory::DynExtractor, Box<dyn std::error::Error>> {
use std::sync::Arc;
use velesdb_memory::extract::DEFAULT_OLLAMA_URL;
use velesdb_memory::OllamaExtractor;
let url = std::env::var("VELESDB_MEMORY_EXTRACTOR_URL")
.unwrap_or_else(|_| DEFAULT_OLLAMA_URL.to_owned());
let model = std::env::var("VELESDB_MEMORY_EXTRACTOR_MODEL").map_err(|_| {
"VELESDB_MEMORY_EXTRACTOR=ollama requires VELESDB_MEMORY_EXTRACTOR_MODEL \
(e.g. qwen3.6:35b-mlx)"
})?;
Ok(Arc::new(OllamaExtractor::new(url, model)))
}
fn build_embedder() -> Result<DynEmbedder, Box<dyn std::error::Error>> {
match std::env::var("VELESDB_MEMORY_EMBEDDER").as_deref() {
Ok("ollama") => build_ollama_embedder(),
Ok("hash") | Err(_) => {
warn_hash_embedder_not_semantic();
Ok(Box::new(HashEmbedder::new(DEFAULT_DIMENSION)))
}
Ok(other) => Err(format!(
"unknown VELESDB_MEMORY_EMBEDDER '{other}' (expected 'hash' or 'ollama')"
)
.into()),
}
}
fn warn_hash_embedder_not_semantic() {
if std::env::var_os("VELESDB_MEMORY_QUIET").is_some() {
return;
}
eprintln!(
"[velesdb-memory] Using the default 'hash' embedder: deterministic and \
fully offline, but NOT semantic — recall matches surface form, not meaning. \
For real semantic recall, run an Ollama build with \
VELESDB_MEMORY_EMBEDDER=ollama (see crates/velesdb-memory/README.md). \
Set VELESDB_MEMORY_QUIET=1 to silence this notice."
);
}
#[cfg(feature = "ollama")]
fn build_ollama_embedder() -> Result<DynEmbedder, Box<dyn std::error::Error>> {
use velesdb_memory::{OllamaEmbedder, DEFAULT_OLLAMA_MODEL, DEFAULT_OLLAMA_URL};
let url = std::env::var("VELESDB_MEMORY_OLLAMA_URL")
.unwrap_or_else(|_| DEFAULT_OLLAMA_URL.to_owned());
let model = std::env::var("VELESDB_MEMORY_OLLAMA_MODEL")
.unwrap_or_else(|_| DEFAULT_OLLAMA_MODEL.to_owned());
Ok(Box::new(OllamaEmbedder::new(url, model)?))
}
#[cfg(not(feature = "ollama"))]
fn build_ollama_embedder() -> Result<DynEmbedder, Box<dyn std::error::Error>> {
Err("the 'ollama' embedder requires building with `--features ollama`".into())
}