Topic keys
Un topic_key es un identificador estable y opcional para un tema que evoluciona. Guardás dos veces con la misma key y el segundo call hace upsert in-place — incrementa revision_count, refresca updated_at, preserva created_at y sobreescribe session_id con la sesión actual.
Por qué existen los topic keys
Sin key, cada mem_save crea una observation nueva (módulo la red de seguridad de hash-dedup). Ese es el default correcto para hechos one-off:
mem.mem_save(
user_id="alice",
type="discovery",
title="FTS5 ignora puntuación al inicio",
content="Descubrí que FTS5 strippea '.', '/', '-' del prefijo...",
)
Pero los temas que evolucionan — el modelo de auth de tu app, una decisión arquitectónica que se va refinando, las preferencias lentas de un usuario — deberían ser una fila que crece en el tiempo, no N filas que tenés que reconciliar después.
# Decisión inicial
mem.mem_save(
user_id="alice",
type="decision",
title="Modelo de auth",
content="Decidimos usar JWT con TTL de 1 hora.",
topic_key="architecture/auth",
)
# Más tarde en la misma conversación — refina la decisión
mem.mem_save(
user_id="alice",
type="decision",
title="Modelo de auth",
content="Decidimos usar JWT con TTL de 1 hora más refresh tokens (TTL de 15 días).",
topic_key="architecture/auth",
)
El segundo call devuelve outcome='updated' — mismo id, revision_count ahora 2, y el timeline muestra la evolución.
Convención de formato
Los topic keys son strings. No hay schema enforcement más allá de "string no vacío". La convención que este proyecto usa (y recomienda) es <area>/<sub-area>:
| Patrón | Ejemplo |
|---|---|
architecture/<area> | architecture/auth, architecture/storage |
bugfix/<area> | bugfix/search-fts5, bugfix/session-timeout |
convention/<area> | convention/naming, convention/error-handling |
user/<id>/<topic> | user/alice/greeting, user/alice/locale |
La convención es solo orientativa — elegí lo que vos y tu equipo entiendan. La librería solo chequea que la key sea no vacía.
Antipatterns
Temas distintos deben usar keys distintas. Reusar la misma key para hechos no relacionados sobreescribe la observation previa. No hay warning, no hay merge, no hay resolución de conflictos — el segundo save gana (outcome='updated'). Es un foot-gun si tratás las keys como labels casuales.
Mal: misma key, dos temas distintos
# Mal — sobreescribe la primera
mem.mem_save(..., title="Modelo de auth", topic_key="architecture")
mem.mem_save(..., title="Capa de storage", topic_key="architecture")
# Resultado: solo sobrevive la de storage, con revision_count=2.
Mal: keys con timestamps o session IDs
# Mal — anula el upsert. Cada call recibe una key única → siempre 'created'.
topic_key=f"decision-{datetime.now().isoformat()}"
Si lo que querés son observations únicas, omití topic_key directamente.
Hash-dedup como red de seguridad
Incluso sin topic_key, la librería te protege contra saves duplicados accidentales. El pipeline:
- Strippea las regiones
<private>delcontent. - Calcula SHA-256 del content post-strip (
normalized_hash). - Busca una observation reciente del mismo usuario con el mismo hash dentro de la ventana de dedup (default: 60 segundos).
- Si la encuentra →
outcome='deduped', sin write, devuelve elidexistente.
Esto atrapa el caso típico de "el agente reintentó el mismo call dos veces" sin inflar el storage.
r1 = mem.mem_save(user_id="alice", type="discovery", title="X", content="Mismo cuerpo")
r2 = mem.mem_save(user_id="alice", type="discovery", title="X", content="Mismo cuerpo")
assert r1.id == r2.id
assert r2.outcome == "deduped"
Hash-dedup corre solo cuando topic_key es None. Si pasás key, gana el path de upsert — mismo content + misma key devuelve deduped (sin write); content distinto + misma key devuelve updated (sobreescribe).
Inspeccionar la evolución: mem_timeline
mem_timeline(user_id, observation_id) devuelve observations temporalmente adyacentes a un anchor, útil para mostrar la historia local alrededor de un tema en una UI.
timeline = mem.mem_timeline(user_id="alice", observation_id=auth_obs_id)
for obs in timeline:
print(obs.created_at, obs.title, obs.revision_count)
Siguiente
- Observations — los estados de outcome (
created/updated/deduped). - Privacy — por qué el hash se calcula después de strippear
<private>. - Architecture / Braess #1 — el invariante privacy ↔ dedup en detalle.