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:
@@ -6,6 +6,55 @@ ratio/diff ver `git show <sha>`.
|
||||
|
||||
## 2026-05-09
|
||||
|
||||
### feat(nous-real): cache de embeddings + write-through al CAS de arje
|
||||
Cierra el ciclo de la crítica del usuario: "Si un archivo no ha
|
||||
cambiado su hash en el CAS, Nouser ni siquiera debería pedirle al
|
||||
LLM que re-genere el embedding". El modelo real
|
||||
(`fastembed-allMiniLML6V2-384d`, ~1-50ms por archivo) era invocado
|
||||
ciegamente en cada re-cluster del watcher. Ahora se cachea por
|
||||
`sha256(bytes-vistos) + model_id`.
|
||||
|
||||
Pipeline en `handle_file`:
|
||||
1. Lee primeros 8 KiB (igual que antes).
|
||||
2. `file_sha = ente_cas::sha256_of(buf)` — hash de los bytes que el
|
||||
modelo *realmente* verá (no del archivo completo). Garantiza
|
||||
que un archivo creciendo más allá de la ventana sin tocar la
|
||||
cabeza siga sirviendo cache hits.
|
||||
3. Cache lookup: HIT → respuesta en ~µs.
|
||||
4. MISS → `ente_cas::store(&buf)` (write-through al CAS de arje,
|
||||
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, así que
|
||||
cambiar de modelo invalida el cache implícitamente. Override por env
|
||||
`NOUSER_NOUS_REAL_CACHE`.
|
||||
|
||||
Encoding compacto: cada `Vec<f32>` se serializa como bytes
|
||||
little-endian (4B por f32, sin overhead). Para el modelo default
|
||||
(384-d) son 1.5 KiB por entry. Decode tolera bytes corruptos
|
||||
(longitud no-múltiplo de 4 → `None`, no panic).
|
||||
|
||||
Por qué 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.
|
||||
|
||||
API:
|
||||
- `EmbedCache::open()` → abre sled, idempotente.
|
||||
- `EmbedCache::open_at(dir)` para tests.
|
||||
- `EmbedCache::get(sha, model)` → `Option<Vec<f32>>`.
|
||||
- `EmbedCache::put(sha, model, &[f32])` → no-fatal en error.
|
||||
- `EmbedCache::len()` → contador para logs (best-effort).
|
||||
|
||||
Mock NO se modifica — su embedding pseudo-32d es metadata-hashing
|
||||
puro, sin costo. Cachearlo sería overhead.
|
||||
|
||||
Tests: 5 unitarios (`roundtrip_returns_same_vector`, `miss_returns_none`,
|
||||
`different_models_do_not_collide`, `different_content_different_keys`,
|
||||
`corrupted_value_returns_none`). Verdes con `--features embeddings`;
|
||||
stub mode (sin feature) sigue compilando sin tocar cache.
|
||||
|
||||
### chore(nakui): alinear `nakui-core` con `[workspace.package]` y deps compartidas
|
||||
Cleanup de drift de convenciones: `nakui-core` era el único crate del
|
||||
monorepo que mantenía `version = "0.1.0"` / `edition = "2021"` /
|
||||
|
||||
Reference in New Issue
Block a user