Memory that degrades gracefully. Not another pile that grows forever.
Current version: 0.6.13
memlife is a four-tier lifecycle memory system for AI agents. Instead of treating memory as a monotonically growing database, every entry has a lifecycle — facts decay, journal entries retire, superseded data is pruned, and nothing accumulates forever.
The four tiers:
- Episodes — raw events (what happened)
- Facts — durable truths (what I know)
- Journal — reflected beliefs (what I believe)
- Decay/Prune — confidence fades, stale entries retire, GC cleans up
Every other memory system accumulates. Facts never expire. Confidence never decays. Stale conventions become unquestioned truths. Recall quality degrades over time.
memlife solves this. Memory should be like human memory — it fades, it gets revised, it gets pruned. Not a database that grows until it breaks.
pip install memlifeWith adapters (optional):
pip install memlife[ollama] # Ollama embedder + chat
pip install memlife[openai] # OpenAI embedder + chat
pip install memlife[sentence-transformers] # Local embeddings
pip install memlife[mcp] # MCP serverimport asyncio
from memlife import MemoryStore, MemoryConfig, DummyEmbedder
async def main():
store = MemoryStore(
config=MemoryConfig(db_path="./memlife.db", embedding_model="dummy"),
embedder=DummyEmbedder(),
)
# Store an episode (something happened)
store.remember(task="User asked about deployment", outcome="success")
# Store a fact (durable truth)
await store.store_fact("User deploys via GitHub Actions", confidence=0.8)
# Store an entity relationship (fact-like but structured)
store.store_triple("User", "deploys_via", "GitHub Actions", confidence=0.8)
# Retrieve relevant memories (unified scoring across all layers)
context = await store.retrieve("deployment")
print(context)
store.close()
asyncio.run(main())No Ollama, no OpenAI, no API key. The DummyEmbedder uses bag-of-words vectors — similar sentences get positive cosine similarity. The full lifecycle — store, retrieve, decay, GC, and entity graph — works without any LLM. Only structured extraction and reflection need a model.
┌───────────┐ reflection ┌───────────┐
│ EPISODE │ ──────────────────▶│ JOURNAL │
│ (event) │ LLM synthesises │ (belief) │
└─────┬─────┘ observations & └─────┬─────┘
│ extract triples │
│ │
│ store_fact() / store_triple() │ confidence decay
▼ │ (configurable)
┌───────────┐ recall bumps ┌─────▼─────┐
│ FACT │ ◀──────────────── │ RETIRE │
│ (truth) │ confidence +0.05 │ (floor) │
└─────┬─────┘ └─────┬─────┘
│ │
│ │ entity graph (triples)
│ ▼
┌───────────────┐
│ TRIPLE / GRAPH│
│(subject-pred- │
│ object + prov)│
└───────┬───────┘
│
│ revise / supersede │ GC prunes
▼ ▼
┌───────────┐ ┌───────────┐
│ SUPERSEDED│ configurable │ PRUNED │
│ (replaced)│ ──────────────────▶│ (deleted) │
└───────────┘ retention └───────────┘
UNIFIED SCORE = relevance × confidence × recency
Applied across ALL layers before every response.
NO-LLM MODE: store + retrieve + decay + GC + entity graph work
without any model. Only reflection and structured extraction need an LLM.
The store, retrieval, decay, GC, entity graph, and embedding versioning all work without any LLM. Only the reflection loop and structured extraction need a model.
from memlife import MemoryStore, MemoryConfig
store = MemoryStore(config=MemoryConfig(db_path="./memlife.db"))
store.remember(task="something happened", outcome="success")
# retrieve() is async — use SyncMemoryStore or asyncio.run():
import asyncio
context = asyncio.run(store.retrieve("something"))
store.close()This makes memlife useful for testing, offline agents, and lightweight deployments where you don't want to manage model access.
Episodes are raw events. They're cheap to store and useful for recall, but the system doesn't treat them as durable truth. Over time, reflection can promote observations from episodes into facts or journal entries.
store.remember(
task="User asked about deployment",
outcome="success",
confidence=0.9,
)Facts are durable truths with explicit confidence. They decay on disuse, get bumped on recall, and can be revised or superseded.
await store.store_fact("User deploys via GitHub Actions", confidence=0.8)Structured relationships between entities. Triples behave like facts but participate in graph traversal.
store.store_triple("User", "deploys_via", "GitHub Actions", confidence=0.8)The journal holds reflected beliefs — higher-level observations the system synthesises from episodes and facts during reflection.
await store.reflect() # or schedule itConfidence decays over time. Entries that fall below a floor retire. Superseded entries age out. Garbage collection prunes them according to retention policy. Everything is configurable.
retrieve() scores across all four tiers using a unified function:
score = relevance × confidence × recency
This means a highly relevant but low-confidence memory won't drown out a moderately relevant, high-confidence, recent one. The result is a blended context string ready to feed into a prompt.
context = await store.retrieve("deployment")from memlife import MemoryConfig
config = MemoryConfig(
db_path="./memlife.db",
embedding_model="dummy", # or "nomic-embed-text", etc.
vector_backend="json", # json | binary | sqlite_vec
reflection_timeout=120.0, # per-LLM-call timeout during reflection
journal_decay_halflife_days=30.0,
journal_decay_floor=0.15,
namespace="default",
)See MemoryConfig for the full set of options.
memlife ships with an MCP server so any MCP-compatible client can use it:
memlife-mcp-server --db-path ./memlife.db --vector-backend binaryTools exposed include memory_store, memory_search, memory_search_journal, memory_search_episodes, memory_retrieve, memory_revise, memory_expire, memory_reflect, memory_gc, memory_vacuum, memory_store_triple, memory_search_triples, and memory_entity_neighbors.
Resources include memlife://stats, memlife://health, and memlife://contradictions.
Note: PyPI vs GitHub
main— The--log-tool-callsflag ships in the current PyPI release (0.5.5 and later). If you are still on 0.5.4, omit--log-tool-callsfrom your MCP server command.Windows / cloud-sync caveat: Do not point
data_dirat a OneDrive, Dropbox, Google Drive, iCloud, or other cloud-sync folder, or at a network share. SQLite WAL mode keeps-waland-shmsidecar files next to the database that are constantly rewritten; sync clients and real-time antivirus scanners can lock or corrupt those files, causing "database is locked" or checksum errors. Use a local, non-synced directory. If you still see locking errors on Windows, setsqlite_journal_mode="DELETE"inMemoryConfigto disable WAL mode.
- Critical fix:
MemoryConfig.fact_merge_threshold/fact_conflict_thresholdare now honoured by the store. Previously the store hardcoded 0.90/0.75 while the Reflector correctly read the config values, so deduplication and conflict detection could disagree. The store now sources both thresholds fromMemoryConfig. - JSONL export/import now covers the full schema.
export_jsonl/import_jsonlpreviously only handledepisodes,facts,journal, andsessions, silently dropping the entity graph (temporal_triples,entities,entity_aliases,triple_provenance), reflection history (agent_runs,checkpoints,reflection_queue,reflection_metrics,reflection_passes), the embedding cache, theepisode_toolsindex, and theis_gap_markerflag. A regression test now asserts every schema table is represented in an export.
MemoryConfig.from_env()validates environment configuration before returning.retrieve()logs and counts recall-path failures instead of silently swallowing them.- Vector backend
delete()rejects unknown table kinds via an explicit allowlist. - MCP server supports
--chat-adapter {ollama,openai}for non-Ollama reflection endpoints. - Polyphonic recall counters now report source attribution per fused candidate.
- GitHub Actions now tests Python 3.11 and 3.12 only; the obsolete Python 3.10 job has been removed.
- Minimum Python version is now 3.11. memlife no longer supports Python 3.10, so the
typing_extensionscompatibility shim added in 0.6.8 has been removed.
- (Superseded by 0.6.10; the Python 3.11 minimum is now documented above.)
- Hardened
_LockedConnthread-safety contract: cursors are now returned as context-managed_LockedCursorproxies that hold the store lock for their entire lifetime, androw_factory,isolation_level, andtext_factoryaccess is serialised through the same lock. - Added PRAGMA allowlist validation in
MemoryConfigandMemoryStore._set_pragma()sojournal_modeand other PRAGMA values are checked before any SQL interpolation. - Audited remaining cursor-iteration sites in
_gc.py,_triples.py, and_schema.pyto use explicit cursors or.fetchall()so cursors never outlive the lock.
- Fixed a silent gap in graph-integrated retrieval: lowercase queries now match stored entities and aliases. Previously
retrieve("james")would not follow graph links for an entity stored as "James" because the query parser only recognised capitalised proper nouns. The extractor is still used for discovering new entities, but retrieval now also scans the query against known entity names and aliases case-insensitively. - Added a regression test for lowercase entity queries in graph retrieval.
- Fixed a case-sensitivity bug in triple queries:
triples_about,triples_from,triples_to,current_truth,truth_as_of, andentity_neighborsnow resolve entities case-insensitively and via aliases, matching the insertion path. Querying "james" returns triples stored under "James" or its alias "Jimmy". - Added a regression test covering mixed-case entity queries and alias resolution.
- Fixed an infinite-loop bug in embedding-cache GC: when the first batch of cache rows were all still referenced, the old
LIMIT/OFFSET-less scan would fetch the same rows forever. GC now uses keyset pagination bycache_key. - Embedding-cache lookup and storage are now batched, cutting round-trips from O(N) per text to 1–2 per batch.
- Auto-extracted entity mention triples and aliases are now committed in a single transaction instead of one commit per entity.
- GC output from the MCP
memory_gctool now includes mention-triple pruning, embedding-cache unreferenced rows, and LRU eviction counts. - Tool-call dedup eviction in the MCP server is now guarded by a lock to avoid a latent race under concurrent tool threads.
- Synchronised
mainbranch with the 0.6.2 and 0.6.3 release tags so the GitHub source tree and README match PyPI.
- README now reports the current version and includes a consolidated "What's New" section.
memlife.__version__andpyproject.tomlversion metadata are kept in sync with releases.
- Graph relationship traversal now follows incoming edges as well as outgoing edges, so querying an entity that appears as the object of a relationship discovers related sources.
- Entity canonicalisation is case-insensitive when creating or ensuring entities. Manual
store_triple("James", ...)reuses an auto-extracted canonical entity "james" instead of creating a duplicate "James" node. MemoryStore.retrieve()andSyncMemoryStore.retrieve()now acceptdebug=Trueand return the structured debug dict.SyncMemoryStore.store_mention_triple()added for parity with the asyncMemoryStoreAPI.- Regression test suite for graph-integrated retrieval added (
tests/test_graph_retrieval.py).
_schema._migrate()now re-reads journal columns before addingannotations_json/links_json, making migration idempotent on partially-migrated databases.FactStore.check_conflicts()keyword fallback now respectsfact_conflict_thresholdinstead of hardcoding 0.5.- Graph-integrated retrieval now scales
graph_signalby the confidence of the strongest currently-valid linking triple, rather than giving every linked candidate a flat 1.0 boost. - Graph retrieval no longer surfaces superseded facts, retired/superseded journal entries, or contradiction rows.
retrieve()now skips vector-recalled episodes that were already added via graph expansion, avoiding duplicate episode candidates.- Graph expansion failures are caught and logged instead of crashing the whole retrieval call.
- Polyphonic recall now runs RRF only on candidates that passed the score cutoff, so cutoff configuration remains meaningful.
- Debug output now includes the actual triples that produced a graph link in
graph_triples.
- Embedding cache — content-addressable, LRU-capped embedding cache.
- Automatic entity extraction — deterministic, zero-LLM extraction of entities from facts, episodes, and journal entries.
- Graph-integrated retrieval —
retrieve()boosts candidates linked to entities mentioned in the query.
- Version consistency.
memlife.__version__now matchespyproject.toml(0.5.5). - README accuracy. The MCP server tool list now lists only implemented
tools and clarifies that
memlife://contradictionsis a resource, not a tool. TheMemoryConfigexample now uses real fields (reflection_timeout,journal_decay_halflife_days,journal_decay_floor). - Bounded tool-call logging cache. The in-memory dedup dict used by
--log-tool-callsis capped so long-running MCP servers cannot grow it without bound. - Idempotent MCP shutdown.
shutdown_mcp_server()now guards against double runs so SIGTERM andatexitcannot both attempt cleanup. - Windows namespace case-collision hardening.
list_namespaces()now normalizes directory names to lowercase and warns/ignores mixed-case duplicates, preventing two directories that resolve to the same database from appearing as separate namespaces on case-insensitive filesystems. - Cloud-sync path warning. memlife now logs a warning when
data_dirresolves under a known cloud-sync folder (OneDrive, Dropbox, Google Drive, iCloud, etc.) because sync clients can lock or corrupt SQLite WAL sidecar files. - Windows sqlite-vec fallback verified. The store already falls back from
sqlite_vectojsongracefully when extension loading is unavailable. On Windows, wherepysqlite3-binaryis not installed, this fallback uses the stdlibsqlite3module and continues to work.
- MCP reflection timeout controls.
memlife-mcp-servernow exposes--reflection-timeoutand--reflection-total-timeoutso reflection stays within the MCP client's window on slower LLM endpoints. - README tool list refreshed. The MCP server tool list now matches the
actually implemented tools, including
memory_recall,memory_revise, andmemory_expire. - Memorias and polyphonic feature flags.
--memorias-extractionand--polyphonic-recallcan now be enabled from the MCP server CLI. - Episode search by tool name.
memory_search_episodesnow accepts atool_namefilter, making it easier to audit how a specific tool was used. - Vector backend default cleanup.
--vector-backendnow defaults tojsonexplicitly, with clearer precedence when legacy flags are also supplied.
- Config validation regression fixed.
MemoryConfig.validate()now runs all invariant checks even whenvector_backendis left at its default auto value. Previously the default ofNonecaused validation to return early, skipping decay and threshold checks. - Binary backend no longer loses JSON-stored vectors.
BinaryVectorBackenddeserialisation now falls back to JSON format for rows stored before the backend was switched, so changing fromjsontobinaryon an existing store does not make old vectors invisible to search.
- Binary vector backend is now used for retrieval. Previously the backend
was selectable but recall still used inline cosine. Fact, episode, and journal
vector recall now route through the configured backend, so
binaryuses Hamming distance search andsqlite_vecuses sqlite-vec KNN. - Vector backend precedence is centralised.
MemoryConfig.resolved_vector_backend()gives a clear order: explicitvector_backend>use_binary_vectors>use_sqlite_vec>jsondefault. Both legacy flags at once now resolve tobinarywith a warning, instead of silently depending on if/else order. - MCP server shutdown is clean. The shutdown path now closes the store, embedder, chat adapter, and reflector, and SIGTERM exits the process after cleanup instead of hanging.
- Ollama adapters no longer create sessions before a loop exists. Sessions
are created lazily on first async call, avoiding
RuntimeErrorwhen the embedder/chat is instantiated outside an event loop.
-
memlife-mcp-servernow accepts--vector-backend {json,sqlite_vec,binary}. -
Pluggable vector backends. Swap between JSON, binary, and
sqlite-vecbackends with a single config change. The new binary backend stores embeddings as bit-packed vectors for ~32x smaller storage, while sqlite-vec provides fast approximate nearest neighbours when the extension is available. -
Public
Metricssnapshot.store.metrics()returns a structured snapshot of counts, embedding coverage, reflection aggregates, recall counters, DB metadata, and schema migration health. Exposed via thememlife://statsMCP resource. -
Schema migration status.
store.migration_status()checks whether the database has all expected tables and columns, flags missing items, and reports SQLite version and page stats. Surfaces inMetricsso operators can detect a stale schema before it breaks. -
pysqlite3 fallback for sqlite-vec. On interpreters whose stdlib
sqlite3is compiled without extension loading, memlife transparently falls back topysqlite3so sqlite-vec can still load. -
Reflection audit trail. Reflection passes are recorded with proposed, kept, and dropped items, plus timing and model metadata. Paginated audit queries help debug what the reflection loop is actually doing.
-
Namespace default and validation hardening. The default namespace is now
_default, names are validated/normalized before any DB is opened, and the config fails fast on unknown vector backends or unsafe paths.
MIT