jottermem
A dead-simple, embeddable memory layer for AI apps — single file, zero infra, zero account. The SQLite of agent memory.
from jottermem import Memory
mem = Memory("agent.db")
mem.remember("The user's favorite color is blue.")
mem.remember("The user works as a software engineer.")
for result in mem.recall("What color does the user like?", k=3):
print(result.score, result.memory.text)
No server, no Postgres, no Neo4j, no Docker Compose, no account. pip install jottermem, one file on disk, done. See examples/quickstart.py for a longer runnable tour (dedup, key-based staleness, forget).
Why
Vector search itself is a solved, commoditized problem (sqlite-vec, LanceDB, ChromaDB). The hard part of "memory" is everything above the vector index: deciding what to store, deduplicating facts over time, resolving conflicts when new information contradicts old, and retrieving the right thing instead of just the nearest thing.
Existing memory frameworks (Mem0, Cognee, Zep, Letta) solve that layer well but assume you're standing up real infrastructure — Postgres, a vector store, optionally Neo4j, Docker Compose — or paying for a hosted platform. jottermem is the missing zero-infra option: everything above lives in one SQLite file, with no required dependency beyond Python itself.
Install
pip install jottermem
That's it — the default embedder and extractor are pure Python with no third-party dependencies. Want real semantic embeddings instead of the lexical default? pip install jottermem[sentence-transformers].
The API
from jottermem import Memory
mem = Memory("agent.db") # one file; ":memory:" for ephemeral/in-process use
# remember() splits input into atomic facts and stores each one
mem.remember("I live in Boston. I work at Acme Corp.")
# recall() does hybrid semantic + keyword search
results = mem.recall("Where does the user live?", k=5)
# forget() and list_memories() for explicit management
mem.forget(results[0].memory.id)
mem.list_memories(filter={"topic": "work"})
Deduplication on write
A new fact whose embedding is nearly identical to an existing active memory is treated as a repeat instead of piling up as a duplicate row:
mem.remember("The user likes tea.")
mem.remember("The user likes tea.") # no-op: touches the existing memory, doesn't duplicate it
Staleness / conflict handling via keys
Facts that change over time (an employer, a preference, a status) should be written with a stable key. Writing a new fact under an existing key marks the old one superseded instead of leaving both active with equal confidence:
mem.remember("Works at Acme Corp.", key="employer")
mem.remember("Works at Globex.", key="employer")
mem.list_memories() # -> just "Works at Globex."
mem.list_memories(include_superseded=True) # -> both, with the old one marked superseded
This is an explicit, honest mechanism, not free-text contradiction detection — inferring conflicts from unstructured text alone is a genuinely unsolved research problem (see the PRD). Tag facts that evolve with a key and jottermem keeps recall pointed at the current value.
Namespaces
Pass namespace= to remember()/recall()/list_memories() (or set one on the Memory instance) to scope memories per user, session, or agent within the same file.
Swapping components
from jottermem import Memory
from jottermem.embeddings import SentenceTransformerEmbedder
mem = Memory("agent.db", embedder=SentenceTransformerEmbedder())
Any object with a dim attribute and a __call__(texts: list[str]) -> list[list[float]] method works as an embedder. Any object with an extract(text: str) -> list[str] method works as an extractor.
For finer-grained atomic facts than the default rule-based sentence splitter, use LLMExtractor — it's provider-agnostic (you supply a complete(prompt) -> str callable), so it adds no dependency and works with any LLM API:
from jottermem import Memory
from jottermem.extraction import LLMExtractor
def complete(prompt: str) -> str:
msg = anthropic_client.messages.create(
model="claude-sonnet-5",
max_tokens=512,
messages=[{"role": "user", "content": prompt}],
)
return msg.content[0].text
mem = Memory("agent.db", extractor=LLMExtractor(complete))
mem.remember("I live in Boston and work at Acme Corp.")
# -> two atomic facts instead of one compound sentence
Accelerating search with sqlite-vec
The default vector search is a brute-force cosine scan in Python — no C extension, works everywhere. Once that stops being fast enough, pip install jottermem[sqlite-vec] accelerates it with a sqlite-vec vec0 index transparently. On a real 419-turn conversation from the LoCoMo benchmark, acceleration took the same remember() + recall() workload from 14.8s to 0.39s — about 38x.
mem = Memory("agent.db") # use_sqlite_vec="auto" by default
"auto" uses the index when it's available and silently falls back to brute-force when it isn't — nothing to configure, and a plain pip install jottermem still works with zero extra dependencies either way. Two things have to be true for acceleration to actually kick in: the sqlite-vec package is installed, and the running Python's sqlite3 module supports loadable extensions (true of python.org and Homebrew builds; not true of Apple's system Python on macOS). Pass use_sqlite_vec=True to raise instead of silently falling back if you need to know acceleration is actually active, or False to disable it outright.
One tradeoff worth knowing: when accelerated, recall()'s keyword-overlap boost re-ranks within the nearest ~50 (or 10 × k, whichever is larger) vector matches rather than every stored memory — an inherent ANN-then-rerank tradeoff. A memory with heavy keyword overlap but a poor vector-similarity rank outside that window won't surface, where the (slower) brute-force path would still find it via the keyword score alone. Dedup and staleness supersession are unaffected — those only need the single nearest match, which the index finds exactly.
MCP server
pip install jottermem[mcp] # requires Python >=3.10, the mcp SDK's own requirement
jottermem-mcp
Exposes remember, recall, forget, and list_memories as MCP tools, so Claude Code and other MCP-aware agents can use jottermem as their own persistent memory directly — no generated Python required. Point the client at the jottermem-mcp command; configure the backing file with JOTTERMEM_DB_PATH (default jottermem.db) and the default namespace with JOTTERMEM_NAMESPACE.
Example client config (Claude Code, .mcp.json or similar):
{
"mcpServers": {
"jottermem": {
"command": "jottermem-mcp",
"env": { "JOTTERMEM_DB_PATH": "/path/to/agent.db" }
}
}
}
The store is safe to call from multiple threads — MCP's SDK runs sync tool handlers in a worker-thread pool, so SQLiteStore opens with check_same_thread=False and serializes access with a lock rather than assuming single-threaded use.
Status
Early / pre-alpha. Working today:
- Single-file SQLite storage (structured metadata + packed embeddings, no separate services)
remember()/recall()/forget()/list_memories()- Dependency-free default embedder (deterministic hashing-trick bag-of-words) and default rule-based sentence extractor
- Deduplication on write, key-based staleness/supersession
- Hybrid recall (cosine similarity + keyword overlap boost)
LLMExtractorfor provider-agnostic, LLM-backed atomic fact extraction- Optional
sqlite-vecacceleration (pip install jottermem[sqlite-vec]), used automatically when available and never required - Two published benchmarks: a synthetic staleness scenario (3/3 vs. 0/3 current-fact accuracy vs. naive top-K) and a real one on LoCoMo's single-hop QA set, where jottermem roughly doubles naive top-K's Recall@k (e.g. 17.2% vs. 8.8% at k=1) across 795 questions on real conversational data, using the same dependency-free embedder on both sides
- An MCP server (
pip install jottermem[mcp]) so agents can use jottermem as memory directly, not just as a library
Published on PyPI. See PRD.md for the full plan and explicit non-goals (this is not trying to be Cognee's graph memory or Mem0 Platform's multi-tenant infra).
Design notes / trade-offs
- Vector search is brute-force cosine in Python by default, accelerated by an optional
sqlite-vecindex (see above) — the brute-force path keeps a plainpip install jottermemgenuinely dependency-free (no C extension wheels that might not exist for your platform, and no dependency onsqlite3being built with loadable-extension support, which Apple's system Python on macOS isn't). It's fine at hundreds of memories; measured at ~500 (see the LoCoMo benchmark above), it's already ~38x slower than the accelerated path — reach forpip install jottermem[sqlite-vec]well before "thousands." - The default embedder is lexical, not semantic — it won't match paraphrases. It's there so
pip install jottermemworks standalone in under 5 minutes with zero infra decisions; swap inSentenceTransformerEmbedderor your own API-backed embedder when recall quality matters more than zero dependencies. - Staleness resolution is key-based, not inferred — see above.
- All stored embeddings are unit-normalized, regardless of what a custom embedder returns — this keeps the sqlite-vec acceleration's Euclidean-to-cosine distance conversion exact for every embedder, not just the bundled ones.
SQLiteStoreis thread-safe, found the hard way: the MCP server runs sync tool calls in a worker-thread pool, and the defaultsqlite3connection isn't usable across threads. The connection opens withcheck_same_thread=Falseand every operation holds a lock.
Releasing
.github/workflows/publish.yml builds and publishes on every GitHub Release via PyPI Trusted Publishing (no stored API token). To cut the next release: bump version in pyproject.toml, then create a GitHub Release with a matching tag.
License
Apache-2.0
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jottermem-0.1.1.tar.gz.
File metadata
- Download URL: jottermem-0.1.1.tar.gz
- Upload date:
- Size: 39.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aefdc43657d0b609aaf6e88fc695bfd83b3d0994a06ae9b32f0b39f46581bc47
|
|
| MD5 |
468bb7888ebae70f3831ccccc9fbb925
|
|
| BLAKE2b-256 |
64feb55eae243ce505aab6c0c8c0d91ffc4f2a01d60c34c425c459933f3982b1
|
Provenance
The following attestation bundles were made for jottermem-0.1.1.tar.gz:
Publisher:
publish.yml on Kazu-Labs/jottermem
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jottermem-0.1.1.tar.gz -
Subject digest:
aefdc43657d0b609aaf6e88fc695bfd83b3d0994a06ae9b32f0b39f46581bc47 - Sigstore transparency entry: 2586966008
- Sigstore integration time:
-
Permalink:
Kazu-Labs/jottermem@dc945dd1add1b6749db28e70c111c6b48b73e676 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/Kazu-Labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@dc945dd1add1b6749db28e70c111c6b48b73e676 -
Trigger Event:
release
-
Statement type:
File details
Details for the file jottermem-0.1.1-py3-none-any.whl.
File metadata
- Download URL: jottermem-0.1.1-py3-none-any.whl
- Upload date:
- Size: 26.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a899714658ce902e6948c37e5f4cc290ba4e22449263f0020c270c4cb7a1a2b5
|
|
| MD5 |
3a5357e7e28d428bc7a39a0ff22018f1
|
|
| BLAKE2b-256 |
37baf9c67c51dfd6ed51815f224d91825c88c8d50867fc58452010e152cd31ba
|
Provenance
The following attestation bundles were made for jottermem-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on Kazu-Labs/jottermem
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jottermem-0.1.1-py3-none-any.whl -
Subject digest:
a899714658ce902e6948c37e5f4cc290ba4e22449263f0020c270c4cb7a1a2b5 - Sigstore transparency entry: 2586966176
- Sigstore integration time:
-
Permalink:
Kazu-Labs/jottermem@dc945dd1add1b6749db28e70c111c6b48b73e676 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/Kazu-Labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@dc945dd1add1b6749db28e70c111c6b48b73e676 -
Trigger Event:
release
-
Statement type: