Saltar al contenido principal

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ónEjemplo
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
tip

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​

aviso

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:

  1. Strippea las regiones <private> del content.
  2. Calcula SHA-256 del content post-strip (normalized_hash).
  3. Busca una observation reciente del mismo usuario con el mismo hash dentro de la ventana de dedup (default: 60 segundos).
  4. Si la encuentra → outcome='deduped', sin write, devuelve el id existente.

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"
nota

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.