All notable changes to grafeo-memory are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Bi-temporal validity tracking, episode-based provenance, and community summaries for richer temporal knowledge graphs.
- Bi-temporal model (opt-in): every Memory node can now carry
valid_at(when the fact became true in reality) andinvalid_at(when it ceased being true), orthogonal to the existing system-time propertiescreated_at/expired_at. Enable withenable_bitemporal=True - Temporal annotation extraction: new LLM step parses real-world dates from message context (ISO-8601, year-only, relative dates) and attaches them to extracted facts. New module
extraction/temporal.pywithannotate_temporal_async() - Point-in-time queries:
search(query, point_in_time=epoch_ms)filters to facts that were valid at a specific moment. Memories with novalid_atare included permissively (backward compatible) - Point-in-time hint detection:
detect_temporal_hints()now recognizes "as of", "at that time", "back then", "in YYYY" patterns and setspoint_in_time_hint=Truewith automatic expired-memory inclusion - Episode provenance (opt-in):
add()creates:Episodenodes storing the raw message text, linked to produced memories viaPRODUCEDedges and to extracted entities viaMENTIONSedges. Enable withenable_episodes=True - Session replay via episodes:
NEXT_EPISODEedges chain episodes within a run. When episodes are enabled,LEADS_TOedges between memories are no longer created (episodes handle session ordering) get_episodes(): retrieve all episodes for a user, optionally filtered bysession_idget_provenance(memory_id): trace any memory back to the raw episode that produced itepisode_chain(episode_id, direction): followNEXT_EPISODEedges forward, backward, or both for session replay- Community summaries (opt-in): after Louvain community detection, creates
:Communitynodes with LLM-generatednameandsummaryfor entity clusters (>= 2 members). Lazy materialization on nextsearch(). Enable withenable_community_summaries=True(requiresenable_graph_algorithms=True) get_communities(): retrieve all detected communities and their summaries- Community context in search: relevant community summaries are discoverable via
get_community_context()for entity-based lookups - New node labels:
Episode,Community - New edge types:
PRODUCED,MENTIONS,NEXT_EPISODE,HAS_MEMBER - New config options:
enable_bitemporal,enable_episodes,enable_community_summaries - New types:
EpisodeResult,CommunityInfo,TemporalAnnotation,TemporalAnnotationOutput,CommunitySummaryOutput - New exports:
EPISODE_LABEL,PRODUCED_EDGE,MENTIONS_EDGE,NEXT_EPISODE_EDGE,COMMUNITY_LABEL,HAS_MEMBER_EDGE,EpisodeResult,CommunityInfo - New modules:
extraction/temporal.py,communities.py - New prompts:
TEMPORAL_ANNOTATION_SYSTEM/USER,COMMUNITY_SUMMARY_SYSTEM/USER graph_namescoping: optionalgraph_nameonMemoryConfigisolates memories and entities per subgraph when multiple callers share a database viadb=. Stamped on all Memory and Entity creates, filtered on all read paths (_build_filters,graph_search,temporal_chain, session linking,stats). No behavior change when unset (#23, #24, thanks @Michaelzag)graph_nameadded to property index list for fast scoped lookups- 15 new tests covering
graph_namewrite stamping, read isolation, entity separation, stats scoping, temporal chain boundaries, and backward compatibility
SearchResultgains two new fields:valid_at,invalid_at(both defaultNone, backward compatible)MemoryEventgainsvalid_atfield (defaultNone, backward compatible)ExtractionResultgainstemporal_annotationsfield (default empty dict, backward compatible)MemoryStatsgainsepisode_countandcommunity_countfields (both default 0)search(),explain()gainpoint_in_timekeyword parameter on bothMemoryManagerandAsyncMemoryManager_expire_memory()setsinvalid_aton the expired node when bi-temporal mode is enabled_execute_decisions()passesvalid_atfrom temporal annotations to new memories and setsinvalid_aton superseded memories_stats_impl()now counts Episode and Community nodesdb_infoin stats includesepisode_node_countandcommunity_node_count- MCP
memory_searchtool gainspoint_in_timeparameter
0.2.1 - 2026-04-07
Test quality, error surfacing, documentation improvements, and shared database support.
- Shared database support:
MemoryManagerandAsyncMemoryManageraccept an optionaldb=keyword argument to inject an existingGrafeoDBinstance instead of creating one internally.close()respects ownership and leaves externally provided databases open (#18, #19, thanks @Michaelzag) - Multi-tenant safety: all internal operations are now scoped to Memory and Entity nodes, making it safe to share a database with non-memory data
- Graph metrics pollution:
_recompute_graph_metrics()now writes_pagerank,_betweenness,_communityonly to Memory/Entity nodes instead of every node in the database - Unlabeled Cypher queries:
temporal_chain()and_get_node_relations()now require:Memorylabels on match patterns, preventing traversal from foreign nodes - N+1 entity lookup:
_find_or_create_entityandgraph_searchreplaced globalfind_nodes_by_property+ Python filtering with single label-scoped Cypher queries, reducing FFI crossings - Stats data leak:
_stats_impl()no longer exposes rawdb.info(), returning only memory-scoped counts. MCPmemory://statsresource now delegates to the manager's scoped stats - Error surfacing tests: broken embedder on add/search now verified to raise, not silently return empty results
- Persistence lifecycle tests: write, close, reopen round-trip with
tmp_path - Multi-session regression tests: sequential open/close/open guards against event loop corruption
- Concurrency tests: concurrent async adds for same user (lock verification) and different users (isolation)
- Reconciliation boundary test: controlled embeddings at threshold boundary verify UPDATE triggers
- History ordering test: LEADS_TO chain verified for session-order retrieval
- Delete cascade test: orphaned entity behavior after memory deletion documented
- Memory type filter test: search with
memory_typefilter verified - Summarize test: non-empty summary from related memories
- MCP tool smoke tests: all tool functions imported and called with mock context
test_search_with_broken_embeddernow usespytest.raisesinstead of silently passing- Concurrency tests use
async withcontext manager for guaranteed cleanup - Error surfacing tests wrapped in
try/finallyfor manager cleanup - Shared
make_managerhelper extracted toconftest.py - README: added model compatibility table (OpenAI, Anthropic, Mistral, Groq, Google)
- README: documented reconciliation threshold tuning and
history()return type - 435 tests passing
0.2.0 - 2026-03-26
Temporal reasoning, multi-hop search and engine integration for better long running memory performance.
- Soft expiry: reconciliation DELETE now sets
expired_atinstead of removing nodes. Old facts remain queryable for point-in-time reconstruction.include_expiredparameter onsearch()andget_all()to access expired memories explicitly - Soft-expiry UPDATE: creates a new memory node and a
SUPERSEDESedge to the expired original, preserving full lineage - Time-range search:
time_beforeandtime_afterparameters onsearch()filter results bycreated_attimestamp - Temporal keyword detection: rule-based heuristics detect "when", "first", "used to", "how many days", etc. in queries. Auto-expands result limits, includes expired memories, and sorts chronologically when appropriate. New
detect_temporal_hints()utility andTemporalHintsdataclass LEADS_TOedges: sequentialadd()calls within a session (run_idorsession_id) create temporal ordering edges, forming causal chains across a conversationtemporal_chain()method: followsLEADS_TOedges forward, backward, or both, with configurable max depth. Available onMemoryManagerandAsyncMemoryManager- 2-hop graph traversal:
graph_search_depth=2config option enablesMemory -> Entity -> RELATION -> Entity -> Memorypaths, surfacing indirectly connected memories. 2-hop results score at 0.7x to prefer direct matches - Graph algorithm scoring:
enable_graph_algorithms=Trueruns PageRank, betweenness centrality, and Louvain community detection after eachadd(). Results cached as node properties (_pagerank,_betweenness,_community), recomputed only when the graph changes - Cross-session entity reinforcement:
cross_session_factorconfig option boosts memories connected to high-PageRank/betweenness nodes, promoting cross-session hubs - MMR diverse search:
search(query, diverse=True)uses the engine'smmr_search()for Maximal Marginal Relevance retrieval, avoiding same-session clustering.mmr_lambdaconfig tunes relevance vs. diversity (default 0.5) - Session-grouped results:
search(query, grouped=True)returns results organized bysession_id, chronologically sorted within each group - Property indexes: automatic
create_property_index()onuser_id,created_at,memory_type, andnameat startup for O(1) filtered lookups - Batch node creation:
_raw_add_batch()usesbatch_create_nodes_with_props()for bulk ingestion, with per-node fallback when unavailable learned_attimestamp: every memory now stores when it was first learned, distinct fromcreated_at(which tracks node creation). Populated onSearchResultsession_idon SearchResult: search results now carrysession_idfor downstream grouping- Native CDC history:
record_history()is a no-op when the engine exposesnode_history()(change data capture).get_history()prefers engine-native CDC events, falling through to legacy:Historynodes - New edge types:
SUPERSEDES_EDGE,LEADS_TO_EDGE - New config options:
graph_search_depth,mmr_lambda,cross_session_factor,enable_graph_algorithms - New exports:
SUPERSEDES_EDGE,LEADS_TO_EDGE,TemporalHints,detect_temporal_hints,apply_cross_session_boost - New module:
temporal.py - 57 new tests across
test_temporal.py,test_multi_hop.py,test_new_features.py, andtest_history.py
- BREAKING: reconciliation DELETE is now soft expiry. Nodes are preserved with
expired_atset. Usemanager.delete()for hard deletion. Existing code callingget_all()orsearch()is unaffected (expired memories excluded by default) - BREAKING: reconciliation UPDATE creates a new node instead of mutating in place. The old node is expired and linked via
SUPERSEDES.MemoryEvent.memory_idnow points to the new node SearchResultgains four new fields:created_at,learned_at,session_id,expired_at(all defaultNone, backward compatible)search()andexplain()gaintime_before,time_after,include_expired,diverse, andgroupedkeyword parametersget_all()gainsinclude_expiredkeyword parameter_get_memories_with_timestamps()now excludes expired memories (affectssummarize())- History module prefers engine CDC when available, reducing graph clutter from
:Historynodes - 387 tests passing, 78% coverage
0.1.5 - 2026-03-14
Guardrails, observability, and search quality: config validation, database introspection, search pipeline tracing, result provenance, score filtering, agreement bonus, threshold semantics fix, and concurrency safety.
MemoryConfig.__post_init__validation: rejects invalid config at construction. Range checks on all weights, thresholds, and dimensions. Warns when importance weights do not sum to ~1.0manager.stats(): returns aMemoryStatsdataclass with memory counts (total, by type), entity count, relation count, and database info. No LLM calls, pure database reads. Available on bothMemoryManagerandAsyncMemoryManagermanager.explain(query): runs a search and returns anExplainResultwith a step-by-step pipeline trace (embedding, hybrid search, entity extraction, graph search, merge, optional boosts). Helps users understand why results rank the way they do- Search result provenance:
SearchResult.sourcefield tracks where each result came from:"vector","graph","both", orNone(forget_all()). Preserved through scoring, reranking, and topology boost stages - Minimum score filtering:
search_min_scoreconfig option (default 0.0) andmin_scoreparameter onsearch()filter out low-quality results. Per-call parameter overrides the config default - Agreement bonus:
agreement_bonusconfig option (default 0.1) gives a 10% score boost when both vector and graph search find the same memory, rewarding cross-source agreement - Embedding dimension validation:
_create_memory()now raisesValueErrorwhen embedding dimensions don't match the vector index, catching model mismatches at write time instead of producing silent search corruption - Per-user locking:
asyncio.Lockper user_id serializes the search-reconcile-store critical section inadd(), preventing race conditions from concurrent calls - CLI subcommands:
stats,explain,--min-scoreon search - MCP tools:
memory_stats,memory_explain_search,min_scoreparameter onmemory_search - New types:
MemoryStats,ExplainStep,ExplainResult - 60+ new tests (config validation, stats, explain, search provenance, min-score filtering, agreement bonus, threshold semantics, dimension validation, locking)
- Silent exception in
scoring.py: barepasson access stats update replaced withlogger.debug()(importance scoring no longer silently loses access counts) - Silent exception in
history.py:get_history()query failures now logged withlogger.warning()instead of returning empty list silently - Entity duplication in
manager.py: narrowed try-block in_find_or_create_entityso property access errors propagate instead of creating duplicate Entity nodes - DERIVED_FROM edge logging: upgraded from
debugtowarninglevel (lineage loss is now visible) - Usage callback error context: log message now includes the callback function name for easier debugging
- BREAKING:
similarity_thresholdrenamed toreconciliation_thresholdwith corrected semantics: now means minimum similarity (0.0-1.0), not maximum distance. Default changed from 0.7 to 0.3 (same effective behavior: olddistance <= 0.7equalssimilarity >= 0.3) - Search merge now uses agreement-aware dedup instead of naive max-score dedup. The
explain()trace includesagreement_countin the merge step search_similar()threshold parameter now uses similarity semantics (higher = stricter) instead of distance semantics- 330 tests passing, 79% coverage
0.1.4 - 2026-02-28
Episodic memory, built-in MCP server, OpenTelemetry tracing, and topology-aware consolidation.
- Episodic memory type: new
memory_type="episodic"for interaction events and reasoning context (e.g. "user asked X, found Y"). Dedicated extraction prompts, filterable insearch()andget_all() - Built-in MCP server (
grafeo-memory-mcp): 9 tools, 2 resources, 2 prompts exposing the high-level memory API to AI agents (Claude Desktop, Cursor, etc.). Install withuv add grafeo-memory[mcp] - OpenTelemetry instrumentation (opt-in): set
instrument=TrueinMemoryConfigto trace all LLM calls via pydantic-ai'sAgent.instrument_all(). Supports customInstrumentationSettingsfor tracer provider and content filtering - Topology-aware consolidation:
consolidation_protect_thresholdconfig option preventssummarize()from merging well-connected hub memories. Memories with topology scores above the threshold are preserved - New
MemoryConfigoptions:consolidation_protect_threshold,instrument - New export:
InstrumentationSettingsfromgrafeo_memory - MCP tools:
memory_add,memory_add_batch,memory_search,memory_update,memory_delete,memory_delete_all,memory_list,memory_summarize,memory_history - MCP resources:
memory://config,memory://stats - MCP prompts:
manage_memories,knowledge_capture - 18 new tests (episodic memory, tracing, MCP tools)
- Dropped Groq from default provider list (Mistral preferred)
- CI now installs all extras (
uv sync --all-extras) to cover MCP tests - All
pip installreferences in docs, examples, and error messages changed touv add(with pip as alternative)
0.1.3 - 2026-02-27
Bug fixes, provenance tracking, and topology-boosted search.
- Provenance edges:
summarize()createsDERIVED_FROMedges linking summary memories to the originals they replaced - Topology boost (opt-in): lightweight search re-ranking that promotes well-connected memories. Enable with
enable_topology_boost=True, tune withtopology_boost_factor(default 0.2). No LLM call, purely structural - Extraction fallback: when combined extraction fails (e.g. Mistral JSON mode), automatically falls back to separate fact + entity extraction calls instead of returning empty
- New config options:
enable_topology_boost,topology_boost_factor - 8 new tests (context manager reuse, extraction fallback, DERIVED_FROM edges, topology boost)
- Context manager reuse: closing a
MemoryManagerand opening a new one in the same process no longer corrupts the async event loop. Theasyncio.Runneris now process-scoped and cleaned up viaatexit - Combined extraction traceback noise: downgraded from
logger.warningtologger.debugsince the fallback is handled gracefully history()return type: now returnslist[HistoryEntry]instead oflist[dict], matching the exported type- Reconciliation logging: fast-path ADD (no existing memories found) now logs at debug level for easier diagnosis
MemoryManager.close()no longer callsshutdown()on the async runnerhistory()return type:list[dict]->list[HistoryEntry](breaking if code accessed dict keys)- CLI
historycommand updated forHistoryEntryattribute access - README API reference rewritten with correct return types and iteration examples
0.1.2 - 2026-02-27
Performance and quality release: fewer LLM calls per operation, smarter memory extraction, and topology-aware scoring.
- Combined extraction: fact + entity + relation extraction now runs in a single LLM call instead of two sequential calls, saving ~1 LLM call per
add(). NewExtractionOutputschema andCOMBINED_EXTRACTION_SYSTEM/COMBINED_PROCEDURAL_EXTRACTION_SYSTEMprompts - Topology-aware scoring (opt-in): graph-connectivity score based on entity sharing between memories. Enable with
weight_topology > 0inMemoryConfig. Inspired by VimRAG - Structural decay modulation (opt-in): foundational memories (those reinforced by newer related memories) resist temporal decay. Enable with
enable_structural_decay=Trueand tunestructural_feedback_gamma. Inspired by VimRAG Eq. 7 - New
MemoryConfigoptions:weight_topology(default 0.0),enable_structural_decay(default False),structural_feedback_gamma(default 0.3) - 20 new topology scoring tests (
test_topology_scoring.py) - 2 new extraction coverage tests (combined extraction error path, vector_search embedding fallback)
- 1 fewer embedding call per search: query embedding is now computed once in
_search()and shared across bothhybrid_search()andgraph_search(), via newquery_embeddingparameter on both functions - Fact grouping prompt: extraction prompts now instruct the LLM to group closely related details into a single fact (e.g., "marcus plays guitar, is learning jazz, and focuses on Wes Montgomery's style" instead of three separate facts), producing fewer but richer memories
- Reconciliation temporal reasoning: reconciliation prompt now includes explicit guidance for temporal updates ("now works at X" → UPDATE), state changes ("car is fixed" → UPDATE "car is broken"), and accumulative facts ("also likes sushi" → ADD alongside "likes pizza")
- Type annotations:
run_sync()now uses generic[T]syntax with properCoroutinetyping instead ofobject -> object - Windows safety net:
_ProactorBasePipeTransport.__del__monkey-patch usescontextlib.suppress(RuntimeError)instead of bare try/except
- Search deadlock on Windows:
_search()no longer triggers nestedrun_sync()calls. Entity extraction for graph search is now performed asynchronously within the already-running event loop, then passed tograph_search()via the new_entitiesparameter. This fixes theRuntimeError: Event loop is closed/ hang when callingsearch()on Windows with Python 3.13+ graph_search()nestedrun_sync(): accepts pre-extracted_entitiesto avoid callingextract_entities()(which internally callsrun_sync()) from within an async context
extract_async()now makes 1 LLM call (combined) instead of 2 (facts → entities). The standaloneextract_facts_async()andextract_entities_async()functions remain unchanged for independent use (e.g., search query entity extraction)graph_search()signature: added_entitiesandquery_embeddingkeyword-only parameters (backward compatible, both default toNone)vector_search()andhybrid_search()signatures: addedquery_embeddingkeyword-only parameter (backward compatible, defaults toNone)compute_composite_score()signature: addedtopologyandreinforcementkeyword-only parameters (backward compatible, both default to 0.0)- Removed local
grafeopath dependency frompyproject.toml([tool.uv.sources]section) - Configured
tychecker: addedextra-paths = ["tests"]and downgraded rules that produce false positives from Rust-extension deps
0.1.1 - 2026-02-12
- CI configuration and failing tests
- Type checking fixes for
ty
- Documentation pass on README
- Lock file updates
0.1.0 - 2026-02-12
Initial release.
MemoryManager(sync) andAsyncMemoryManager(async): full memory CRUD withadd(),search(),update(),delete(),get_all(),history()- LLM-driven extraction pipeline: fact extraction, entity/relation extraction via pydantic-ai structured output
- LLM-driven reconciliation: ADD / UPDATE / DELETE / NONE decisions for new facts against existing memories, plus relation reconciliation for graph edges
- Hybrid search: BM25 + vector similarity with RRF fusion, falling back to vector-only when hybrid is unavailable
- Graph search: entity extraction from queries, graph traversal via HAS_ENTITY edges, cosine similarity scoring
- Importance scoring (opt-in): composite scoring with configurable weights for similarity, recency, frequency, and importance
- Memory summarization: LLM-driven consolidation of old memories into fewer, richer entries
- Procedural memory: separate memory type for instructions, preferences, and behavioral rules with dedicated extraction prompts
- Vision / multimodal (opt-in): describe-first approach for image inputs via LLM vision
- Actor tracking: optional
actor_idandroleon messages for multi-agent scenarios - Scoping:
user_id,agent_id,run_idfor multi-tenant memory isolation - Usage tracking (opt-in): per-step LLM usage callbacks via
usage_callback - CLI:
grafeo-memory add,search,list,update,delete,history,summarizewith JSON output mode - Graph-native history: HAS_HISTORY edges tracking all memory mutations with actor and timestamp
- Windows async compatibility: persistent
asyncio.RunnerandProactorEventLoopsafety net for Python 3.13+ - 230 tests, 83% coverage