The test
This is the payoff. session15_test.py seeds six facts in session 1, moves the user in session 8, and interrogates the store in session 15 — no LLM in the assertions, just the memory layer, so the result is deterministic.
# session15_test.py
from memory import connect, write_fact, recall
con = connect("test.db")
# --- Session 1: six things about the user ---
session1 = [
("name", "Maya"),
("role", "backend engineer"),
("city", "Toronto"),
("project", "the billing rewrite"),
("preference", "terse answers, no preamble"),
("constraint", "no cloud vendors"),
]
for pred, val in session1:
write_fact(con, pred, val, session=1)
# --- Session 8: one fact changes ---
write_fact(con, "city", "Berlin", session=8)
# --- Session 15: ask ---
print("Q: Where do I live?")
print(" ->", recall(con, "where do I live?", k=1))
print("\nQ: What do you remember about me?")
for s, p, v in recall(con, "tell me everything about the user", k=6):
print(f" - {p}: {v}")$ python session15_test.py
Q: Where do I live?
-> [('user', 'city', 'Berlin')]
Q: What do you remember about me?
- name: Maya
- role: backend engineer
- city: Berlin
- project: the billing rewrite
- preference: terse answers, no preamble
- constraint: no cloud vendorsBoth questions pass. Six facts recalled, and "where do I live?" returns Berlin, not the stale Toronto — because the write path in Step 4 closed the Toronto row when the move came in. That single UPDATE ... SET valid_to is the difference between a memory that searches the past and one that knows the present.
What you built
- A temporal fact store in one SQLite file — facts with validity intervals, so change is a first-class operation.
- An extract → embed → upsert write path that keeps only durable facts and retires stale ones.
- A valid-only recall that can't surface a fact that's no longer true.
- A rolling-summary layer for the fuzzy context the facts don't hold.
- A memory manager that runs all four operations around a normal agent turn — fully offline via Ollama.
Run the session-15 test against your own code and it passes the exact scenario the companion drip uses to separate the strategies.
Where to take it next
- Skip identical writes. Guard
write_factso re-stating the same value doesn't churn a new row. - Confidence & decay. Add a
confidencecolumn; let low-confidence facts expire on a schedule rather than living forever. - Scale the recall. Past ~10k facts, replace the Python cosine scan with
sqlite-vec— samerecall()signature, an indexed ANN search underneath. - Learn the write policy. The frontier direction from the drip: instead of a hand-written extractor, train when to write and what to keep with RL, à la MemAgent. Overkill for a personal agent, essential at scale.
- Share it as a tool. Wrap
recallandwrite_factbehind an MCP server so any agent — Claude Code, your own harness — can read and write the same memory.
The memory layer is the seam everything else hangs off. Build it once, and every session after the first starts warm.
Reference: Agent Long-Term Memory (drip) · sqlite-vec · MemAgent (RL-trained memory) · Build Your Own MCP Server (blueprint)