Architecture
iLAB Memory está construido alrededor de dos decisiones estructurales y seis invariantes cross-feature (los "Braess points"). Si solo vas a leer una página de Concepts para integrar la librería sin romper nada, esta es.
Hexagonal — Store ABC + fachada
La librería sigue un patrón hexagonal: una clase abstract base Store define las operaciones de persistencia; un único SQLiteStore las implementa; el orchestrator ILabMemory es el único módulo que importa Store.
┌──────────────────────────────────────────┐
│ Tu app ──▶ ILabMemory (fachada) │
│ │ │
│ ▼ │
│ Store (ABC) │
│ ▲ │
│ │ │
│ SQLiteStore (impl) │
│ │ │
│ ▼ │
│ Archivo SQLite + FTS5 │
└──────────────────────────────────────────┘
Por qué te importa:
- Importás
ILabMemory. Nada más. Los 8 métodos públicos (mem_session_start,mem_session_end,mem_session_summary,mem_save,mem_search,mem_get_observation,mem_timeline,mem_stats) cubren toda operación. from_path("...")es el modo fácil. Es la excepción documentada one-off que te cableaSQLiteStoreautomáticamente.- Los servidores HTTP y MCP mapean 1:1 a estos métodos y serializan los mismos modelos Pydantic. Hay una sola fuente de verdad.
Los 6 Braess points
El product map identifica seis zonas de interferencia cross-feature — fronteras donde se encuentran dos features y un assumption equivocado rompe ambas. La librería codifica cada una como invariante duro; los tests las custodian; y deberías saber que existen antes de extender el código.
Braess #1 — privacy-tags ↔ upsert-dedup
Invariante: el normalized_hash se calcula sobre el content post-strip, nunca sobre el input crudo.
Por qué importa: si hashearas el content crudo, dos saves cuya única diferencia fuera un bloque <private> producirían hashes distintos — anulando el dedup y filtrando la existencia del secreto a través de colisiones de hash.
Dónde: upsert.py::upsert_observation llama primero a strip_private, después a compute_normalized_hash. Tests a nivel AST aseguran que ningún otro path llega a la función de hash.
Ver Privacy para el detalle completo.
Braess #2 — session-lifecycle ↔ memory-context
Invariante: cada session summary lleva un boolean is_auto_generated. Los summaries sintéticos del auto-close están flagueados True; los humanos de mem_session_end están flagueados False.
Por qué importa: el summary auto es una lista de fragmentos [type] title — útil para hidratar pero no narrativo. Las UIs y el código de hidratación al LLM tienen que distinguir los dos antes de mostrarlo a un usuario.
Dónde: Session.is_auto_generated y SessionSummaryCompact.is_auto_generated son required, no defaulted.
Ver Sessions para el flujo de auto-close.
Braess #3 — scoring search ↔ scoring context
Invariante: recency_score y revision_score están definidas una sola vez en scoring.py y son llamadas por las dos fórmulas composite. Agregar un signal nuevo a un composite requiere actualizar el otro en el mismo PR.
Por qué importa: helpers divergentes sesgarían silenciosamente un ranking respecto del otro, haciendo los cambios imposibles de razonar.
Dónde: scoring.py exporta los helpers. search_score y context_score son los únicos composites que los usan, con las tuplas explícitas SEARCH_WEIGHTS y CONTEXT_WEIGHTS.
Ver Scoring.
Braess #4 — upsert ↔ session-lifecycle
Invariante: cuando un upsert hace UPDATE de una observation existente, el session_id persistido se sobreescribe con la sesión actual. La librería nunca lee el session_id de la fila existente.
Por qué importa: una observation vive en la sesión de su última actualización material (CREATE o UPDATE), no en la de su primera aparici ón. Eso hace de "mostrame todas las memorias de la sesión X" una query limpia y evita que session links viejos contaminen el contexto.
Nota: los paths DEDUPED no tocan session_id (no hay write). La sesión igual se toca a nivel del orchestrator (D8: mem_save siempre extiende la sesión activa).
Dónde: upsert.py::upsert_observation paso 4b — model_copy(update={..., "session_id": new_obs.session_id, ...}).
Braess #5 — memory-context ↔ search
Invariante: SearchScore y ContextScore son clases Pydantic frozen distintas. mypy se rehúsa a asignar una a la otra; los isinstance checks en runtime fallan en ambas direcciones.
Por qué importa: sus valores cubren el mismo rango [0, 1] pero significan cosas distintas. Un sort naive que los mezcle produce rankings sin sentido.
Dónde: scoring.py declara las dos clases; el campo ObservationCompact.score: float lleva una u otra según qué API la produjo. El docstring del modelo lo advierte explícitamente.
Ver Scoring para la regla "no comparar".
Braess #6 — api-http ↔ api-mcp
Invariante: ambas superficies de API serializan desde los mismos modelos Pydantic (SaveResult, SessionStartResponse, ObservationCompact, ObservationPublic, SessionSummaryCompact). No hay un shape JSON paralelo armado a mano.
Por qué importa: el server HTTP REST y el server MCP stdio son dos transportes para la misma librería. Si divergieran, los callers verían campos distintos, defaults distintos, errores de validación distintos según el transporte.
Dónde: api/http.py y api/mcp.py ambos importan de core/models.py. ObservationPublic existe específicamente para darle a las superficies wire un modelo que excluya normalized_hash (solo interno).
Ver Observations para el split Compact / Public.
Implicaciones para integradores
Si tratás iLAB Memory como caja negra y solo llamás los 8 métodos de la fachada, no necesitás pensar en los Braess points día a día — están protegidos por tests. Lo que sí necesitás tener presente:
- Los scores de
mem_searchymemories[]no son comparables. Sortear una lista mezclada es undefined behavior a nivel semántico. - Envolvé secretos en
<private>proactivamente. El strip es regex preciso, pero no puede adivinar qué cuenta como sensible. - Los topic keys son namespaces. Reusar uno entre temas no relacionados sobreescribe silenciosamente.
- Las sesiones se reutilizan a demanda. Llamar
mem_session_startes barato e idempotente — llamalo en cada evento "el usuario apareció".
Siguiente
- Observations, Sessions, Topic keys, Scoring, Privacy.
- API Reference (Fase 4 — próxima) para los schemas Pydantic completos, OpenAPI y la lista de tools MCP.