use std::sync::Arc;
use super::helpers;
use super::{MapCache, ServerState};
pub(super) async fn run_background_gc(state: Arc<ServerState>) {
if state.store.read().await.blobs_shared {
tracing::debug!("background blob GC skipped: blob cache is shared across git worktrees");
return;
}
let result = tokio::task::spawn_blocking(move || {
let store = state.store.blocking_read();
let referenced = crate::store_gc::collect_referenced_hashes(&store.basemind_dir)?;
crate::store_gc::gc_blobs(&store.basemind_dir, &referenced)
})
.await;
match result {
Ok(Ok(report)) if report.removed > 0 => tracing::info!(
removed = report.removed,
bytes_freed = report.bytes_freed,
"background blob GC reclaimed orphaned blobs"
),
Ok(Ok(_)) => tracing::debug!("background blob GC: nothing to reclaim"),
Ok(Err(error)) => tracing::warn!(%error, "background blob GC failed"),
Err(error) => tracing::warn!(%error, "background blob GC task panicked"),
}
}
pub(super) fn spawn_initial_scan(state: Arc<ServerState>) {
tracing::info!("empty index on startup; running initial scan in background");
tokio::spawn(async move {
use std::sync::atomic::Ordering;
state.initial_scan_active.store(true, Ordering::Relaxed);
let started = std::time::Instant::now();
match helpers::scan_and_refresh(Arc::clone(&state), None, crate::scanner::EmbedMode::Deferred).await {
Ok(report) => tracing::info!(
scanned = report.stats.scanned,
updated = report.stats.updated,
elapsed_ms = started.elapsed().as_millis() as u64,
"initial background scan complete (code-map + keyword lane; embeddings deferred)"
),
Err(error) => tracing::warn!(%error, "initial background scan failed"),
}
state
.initial_scan_ms
.store(started.elapsed().as_millis() as u64, Ordering::Relaxed);
state.initial_scan_active.store(false, Ordering::Relaxed);
let embed_state = Arc::clone(&state);
tokio::spawn(async move {
let embed_started = std::time::Instant::now();
tracing::info!("background embedding pass starting");
match helpers::scan_and_refresh(Arc::clone(&embed_state), None, crate::scanner::EmbedMode::Inline).await {
Ok(report) => tracing::info!(
scanned = report.stats.scanned,
updated = report.stats.updated,
elapsed_ms = embed_started.elapsed().as_millis() as u64,
"background embedding pass complete"
),
Err(error) => tracing::warn!(%error, "background embedding pass failed"),
}
run_background_gc(embed_state).await;
});
});
}
pub(super) fn spawn_serve_watcher(state: Arc<ServerState>) {
let root = state.root.clone();
let config = Arc::clone(&state.config);
let handle = tokio::runtime::Handle::current();
let (_shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
std::thread::Builder::new()
.name("basemind-mcp-serve-watcher".to_string())
.spawn(move || {
let _keep_sender_alive = _shutdown_tx;
tracing::info!(root = %root.display(), "serve watcher armed (live incremental rescan)");
let result = crate::watcher::watch_paths(&root, &config, shutdown_rx, |paths, _kind| {
let refresh_state = Arc::clone(&state);
match handle.block_on(helpers::scan_and_refresh(
refresh_state,
Some(paths),
crate::scanner::EmbedMode::Inline,
)) {
Ok(report) => tracing::debug!(
scanned = report.stats.scanned,
updated = report.stats.updated,
removed = report.stats.removed,
"serve watcher: incremental rescan complete"
),
Err(error) => tracing::warn!(
%error,
"serve watcher: incremental rescan failed (watcher continues)"
),
}
});
if let Err(error) = result {
tracing::warn!(%error, "serve watcher exited with error");
}
tracing::info!("serve watcher: exiting");
})
.ok();
}
pub(super) fn spawn_view_watcher(state: Arc<ServerState>) {
let (basemind_dir, view) = {
let store = match state.store.try_read() {
Ok(g) => g,
Err(_) => return,
};
(store.basemind_dir.clone(), store.view.clone())
};
let view_dir = basemind_dir.join(crate::store::VIEWS_DIR).join(&view);
let target = view_dir.join(crate::store::INDEX_FILE);
std::thread::Builder::new()
.name("basemind-mcp-view-watcher".to_string())
.spawn(move || {
use notify_debouncer_full::new_debouncer;
use std::time::Duration;
let (tx, rx) = std::sync::mpsc::channel();
let mut debouncer = match new_debouncer(Duration::from_millis(150), None, tx) {
Ok(d) => d,
Err(e) => {
tracing::warn!(error = %e, "view watcher: failed to start debouncer");
return;
}
};
if let Err(e) = debouncer.watch(&view_dir, notify::RecursiveMode::NonRecursive) {
tracing::warn!(error = %e, dir = %view_dir.display(), "view watcher: failed to watch");
return;
}
tracing::info!(target = %target.display(), "view watcher armed");
while let Ok(result) = rx.recv() {
let events = match result {
Ok(e) => e,
Err(_) => continue,
};
let touches_index = events.iter().any(|de| de.event.paths.iter().any(|p| p == &target));
if !touches_index {
continue;
}
let new_store = match crate::store::Store::open_read_only(
state.root.as_path(),
&state.store.try_read().map(|g| g.view.clone()).unwrap_or_default(),
) {
Ok(s) => s,
Err(e) => {
tracing::warn!(error = %e, "view watcher: store reopen failed");
continue;
}
};
let new_cache = Arc::new(MapCache::build(&new_store));
tracing::info!(
files = new_cache.by_path.len(),
"view watcher: rebuilt MapCache from refreshed index"
);
state.cache.store(new_cache);
state
.cache_generation
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
tracing::info!("view watcher: channel closed; exiting");
})
.ok();
}