feat(nous-real): cache de embeddings + write-through al CAS de arje
Cierra el ciclo del feedback: el modelo real (fastembed-allMiniLML6V2, ~1-50ms por archivo) era invocado ciegamente en cada re-cluster del watcher. Ahora se cachea por sha256(bytes-vistos) + model_id, con write-through al CAS de arje. Pipeline en handle_file: 1. Lee primeros 8 KiB del archivo (igual que antes). 2. file_sha = ente_cas::sha256_of(buf) — hash de los bytes que el modelo *realmente* verá. Garantiza que un archivo creciendo mas alla de la ventana sin tocar la cabeza siga sirviendo cache hits. 3. Cache lookup -> HIT: respuesta en us, sin invocar fastembed. 4. MISS: ente_cas::store(&buf) (write-through, no-fatal si falla) -> backend.embed_one(text) -> cache.put(...). Backend de cache: sled local en $XDG_CACHE_HOME/brahman/nouser-nous-real-embed-cache.sled. Tree versionado embed_cache_v1; el MODEL_ID viaja en la key, asi que cambiar de modelo invalida el cache implicitamente. Override por env NOUSER_NOUS_REAL_CACHE. Encoding compacto: cada Vec<f32> se serializa como bytes little-endian (4B por f32, sin overhead). Para 384-d son 1.5 KiB por entry. Decode tolera bytes corruptos (longitud no-multiplo de 4 -> None, no panic). Por que sled y no ente-cas directo: el CAS de arje es flat sha256-keyed; la cache necesita un mapeo (file_sha, model_id) -> embedding, no expresable como entry CAS. El write-through a CAS queda como registro consultable + futura GC. Mock NO se modifica — su embedding pseudo-32d es metadata-hashing puro, sin costo. Cachearlo seria overhead. Tests: 5 unitarios verdes (roundtrip, miss, model collision, content collision, corrupted value). Stub mode (sin feature) sigue compilando sin tocar cache.
This commit is contained in:
@@ -40,6 +40,8 @@ use nouser_nous::{transport, FLOW_EMBED_REQUEST, FLOW_EMBED_RESULT, FLOW_TYPE_NA
|
||||
use tokio::net::UnixListener;
|
||||
use tracing::info;
|
||||
|
||||
#[cfg(feature = "embeddings")]
|
||||
mod cache;
|
||||
#[cfg(feature = "embeddings")]
|
||||
mod embeddings;
|
||||
#[cfg(not(feature = "embeddings"))]
|
||||
@@ -90,15 +92,30 @@ async fn main() -> std::io::Result<()> {
|
||||
#[cfg(feature = "embeddings")]
|
||||
let backend = std::sync::Arc::new(backend);
|
||||
|
||||
// 4. Accept loop.
|
||||
// 4. Abrir el cache de embeddings (sled local, sha256-keyed).
|
||||
// Si falla, seguimos sin cache — degrada a "siempre embed".
|
||||
#[cfg(feature = "embeddings")]
|
||||
let embed_cache = match cache::EmbedCache::open() {
|
||||
Ok(c) => {
|
||||
info!(entries = c.len(), "embed-cache abierto");
|
||||
Some(c)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "embed-cache no disponible — todas las requests irán al modelo");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// 5. Accept loop.
|
||||
loop {
|
||||
let (stream, _addr) = listener.accept().await?;
|
||||
|
||||
#[cfg(feature = "embeddings")]
|
||||
{
|
||||
let backend = backend.clone();
|
||||
let cache = embed_cache.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = embeddings::handle_conn(stream, backend).await {
|
||||
if let Err(e) = embeddings::handle_conn(stream, backend, cache).await {
|
||||
tracing::warn!(error = %e, "conn falló");
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user