Topic keys
A topic_key is an optional, stable identifier for an evolving topic. Save with the same key twice and the second call upserts in place — bumping revision_count, refreshing updated_at, preserving created_at, and overwriting session_id with the current session.
Why topic keys exist
Without a key, every mem_save call creates a brand new observation (modulo the hash-dedup safety net). That is the right default for one-off facts:
mem.mem_save(
user_id="alice",
type="discovery",
title="FTS5 ignores leading punctuation",
content="Found out FTS5 strips '.', '/', '-' from the prefix...",
)
But evolving topics — your app's auth model, an architectural decision being refined, a user's slowly-changing preferences — should be one row that grows over time, not N rows that you have to reconcile later.
# Initial decision
mem.mem_save(
user_id="alice",
type="decision",
title="Auth model",
content="Decided to use JWT with 1-hour TTL.",
topic_key="architecture/auth",
)
# Later in the same conversation — refines the decision
mem.mem_save(
user_id="alice",
type="decision",
title="Auth model",
content="Decided to use JWT with 1-hour TTL plus refresh tokens (15-day TTL).",
topic_key="architecture/auth",
)
The second call returns outcome='updated' — same id, revision_count is now 2, and the timeline shows the evolution.
Format convention
Topic keys are strings. There is no schema enforcement beyond "non-empty string". The convention this project uses (and recommends) is <area>/<sub-area>:
| Pattern | Example |
|---|---|
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 |
The convention is purely advisory — pick whatever you and your team understand. The library only checks that the key is non-empty.
Anti-patterns
Different topics must use different keys. Reusing the same key for unrelated facts will overwrite the previous observation. There is no warning, no merge, no conflict resolution — the second save wins (outcome='updated'). This is a foot-gun if you treat keys as casual labels.
Don't: same key, two different topics
# Bad — overwrites the first one
mem.mem_save(..., title="Auth model", topic_key="architecture")
mem.mem_save(..., title="Storage layer", topic_key="architecture")
# Result: only the storage one survives, with revision_count=2.
Don't: keys with timestamps or session IDs
# Bad — defeats the upsert. Each call gets a unique key → always 'created'.
topic_key=f"decision-{datetime.now().isoformat()}"
If you want unique observations, just omit topic_key entirely.
Hash-dedup as a safety net
Even without topic_key, the library protects you against accidental duplicate saves. The pipeline:
- Strip
<private>regions fromcontent. - Compute SHA-256 of the post-strip content (
normalized_hash). - Look for a recent observation by the same user with the same hash inside the dedup window (default: 60 seconds).
- If found →
outcome='deduped', no write, return existingid.
This catches the common "agent retried the same call twice" scenario without inflating storage.
r1 = mem.mem_save(user_id="alice", type="discovery", title="X", content="Same body")
r2 = mem.mem_save(user_id="alice", type="discovery", title="X", content="Same body")
assert r1.id == r2.id
assert r2.outcome == "deduped"
Hash-dedup runs only when topic_key is None. If you provide a key, the upsert path takes precedence — same content + same key returns deduped (no write); different content + same key returns updated (overwrite).
Inspecting evolution: mem_timeline
mem_timeline(user_id, observation_id) returns observations temporally adjacent to an anchor, useful for showing the local history around a topic in a 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)
Next
- Observations — outcome states (
created/updated/deduped). - Privacy — why the hash is computed after stripping
<private>. - Architecture / Braess #1 — the privacy ↔ dedup invariant in detail.