How are memories updated, consolidated or forgotten?
Update/merge logic; summarisation or consolidation; decay, TTL and deletion; versioning or history.
Verdict
Graphiti handles changing facts best: a contradicting fact closes the old edge with an invalid_at date instead of deleting it. Hindsight and Honcho go furthest at consolidation, with background LLM jobs that merge facts into higher-level beliefs.
Background consolidation. After each retain, Hindsight asks an LLM to create, update or delete “observations”, and tracks proof_count and source ids for each one. Recency affects ranking only, and nothing expires. Honcho “dreams” after 50 new explicit facts and at least 8 hours since the last dream. A deduction agent can delete conclusions and rewrite the peer card, and an induction agent adds generalisations. Its surprisal sampling is off by default. Cognee re-extracts only the changed chunks on update(), and its improve() pass feeds sessions and feedback back into the graph. MemMachine asks an LLM which features to keep once a tag holds 20. The consolidation_threshold setting is never passed through, so the threshold is always 20. Features keep no history.
Time-stamped invalidation. Graphiti closes contradicted edges with invalid_at and expired_at. Its remove_episode is partial: it does not restore edges that the removed episode had invalidated.
Append and count. Mem0 only adds memories. It logs explicit updates and deletes in SQLite, and decay is a hosted-only feature. Memori increments num_times when a fact repeats exactly, has no contradiction handling, and deletes per entity, all or nothing. claude-mem never forgets by age. Its ACT-R ranking and cross-session dedup are both off by default. MemOS merges conflicting memories only when its reorganizer is enabled, which it is not by default. A feedback endpoint can rewrite memories through an LLM.
Hidden. Supermemory’s types show version chains, isForgotten and forgetAfter, but the logic runs on the closed server.
Pick: Graphiti when you need to ask what was true at a given time. Pick: Hindsight or Honcho for memory that consolidates on its own. Pick: Mem0 or Memori when your application owns corrections.
Per-project answers
thedotmack/claude-mem
answeredUpdate/merge: Memory items support full field-level replacement via MemoryItemsRepository.update (src/storage/sqlite/memory-items.ts:181-243), which merges provided fields over existing values. The dedup system (src/services/dedup/nearDuplicate.ts:49-76) classifies observation pairs into two tiers: Tier-0 exact — normalized titles match → silent auto-merge; Tier-1 candidate — IDF-weighted TF-IDF cosine similarity above a configurable threshold and no IDF-veto fires → flagged as a candidate for review but never auto-merged (src/services/dedup/tfidfCosine.ts:20-34). The IDF veto (src/services/dedup/idfVeto.ts) prevents merging when a rare discriminating token appears in only one of the two observations. Consolidation: Session summaries condense the entire session arc into request/investigated/learned/completed/next_steps fields (processSessionSummaryResponse at src/server/generation/processGeneratedResponse.ts:199-256). These coexist alongside per-event observations rather than replacing them. Cleanup and deletion: The Postgres schema enforces cascade deletes — deleting a team or project cascades to sessions, events, observations, and generation jobs (src/storage/postgres/schema.ts:91-238). The CleanupV12_4_3 utility (src/services/infrastructure/CleanupV12_4_3.ts:27-236) runs one-time cleanup of stale observer sessions with automatic database backup before deletion. FTS maintenance (src/services/infrastructure/FtsMaintenance.ts:60-84) merges FTS5 b-trees incrementally to reclaim space from deleted rows. No explicit TTL/decay: The system relies on recency-based selection and optional ACT-R reinforcement scoring (src/services/reinforcement/rank.ts:50-63) to naturally demote older observations, rather than expiring them. Observations remain in storage indefinitely but are excluded from the context budget window by newer content. Generation jobs have configurable max_attempts with exponential backoff (src/server/generation/processGeneratedResponse.ts:556-560).
mem0ai/mem0
answeredUpdate/merge — update() (mem0/memory/main.py:1829-1881) retrieves the existing memory by ID, re-embeds the new text, freshens the payload (recomputing hash and lemmatized text, updating updated_at), and calls vector_store.update(). Identity keys (user_id/agent_id/run_id) are immutable — metadata attempts to change them are silently dropped with a warning via _strip_identity_keys (mem0/memory/main.py:143-162). If the text changed, the old entity links are removed and new entities extracted via _link_entities_for_memory. The change is recorded in the SQLite history table as an UPDATE event with the old text preserved.
Deletion — delete() (mem0/memory/main.py:1883-1902) retrieves the existing memory, removes it from the vector store, and strips its memory_id from linked entity records via _remove_memory_from_entity_store. The history entry records the event as a DELETE with is_deleted=1. delete_all() (mem0/memory/main.py:1904-1958) lists memories in batches of 1000 and deletes each one, with a guard against infinite loops on stores that cap list() results.
Expiration/TTL — the expiration_date parameter on add() and update() sets a YYYY-MM-DD date in the payload. _payload_is_expired() (mem0/memory/main.py:442-451) checks whether that date is before today (UTC). Expired memories are excluded from search() and get_all() unless show_expired=True. There is no automated background cleanup — expired records remain in the store but are hidden.
No decay or consolidation — the OSS SDK explicitly rejects decay features. Calling project.update(decay=True) raises ValueError with the message that decay is a hosted-platform-only feature. Neither consolidation, summarization, nor automated forgetting across memories exists in the self-hosted path.
Versioning/history — history(memory_id) (mem0/memory/main.py:1960-1973) queries the SQLite history table for every ADD, UPDATE, and DELETE event on a given memory, ordered by timestamp. Each record stores the old and new text, the event type, timestamps, actor_id, and role.
Reset — reset() (mem0/memory/main.py:2144-2174) deletes the vector store collection and SQLite database, then recreates both from scratch. The entity store (if initialized) is also reset.
vectorize-io/hindsight
answeredLifecycle management has three main paths. Consolidation (consolidation/consolidator.py:1425-1544) runs as a background job after retain, processing unconsolidated memories (memory_units where consolidated_at IS NULL). It groups new facts, fetches existing observations by embedding similarity, and calls the LLM with a consolidation prompt (prompts.py:1-80) that decides whether to CREATE a new observation, UPDATE an existing one (merging new evidence), or DELETE a stale one. Observations are stored in memory_units with fact_type='observation', tracking proof_count and source_memory_ids. The consolidator also runs LLM-based semantic dedup (_dedup_adjudicate, line 275) with a configurable consolidation_dedup_threshold that probes nearest neighbours and asks the LLM if the new observation is truly distinct (using _DEDUP_PROMPT, line 178-197). Dedup at write time via the entity resolver uses trigram similarity clustering (entity_resolver.py:79-80) to merge near-identical entity names within a batch. Mental model (knowledge page) refresh turns a synthetic document into a curated summary: it runs reflect on a configured source_query scope and writes the result as markdown content in the knowledge_pages table. Pages can refresh on a cron schedule or after each consolidation round. Explicit delete (delete_memory_unit, memory_engine.py:10936-10985) cascades to memory_links, unit_entities, and invalidates dependent observations. Direct invalidation (invalidate_memory endpoint) marks a memory unit as invalidated in invalidated_memory_units so consolidation can re-process its dependents. There is no TTL-based decay — the recency boost in reranking (reranking.py:35-53) is a query-time scoring adjustment (linear decay over 365 days, exponential with 90-day half-life, or disabled), not a deletion mechanism. Version history is tracked on mental models (mental_model_versions table) and on the consolidation history JSONB column per observation.
getzep/graphiti
answeredUpdate/merge happens at write time via the dedup pipeline: when resolve_extracted_nodes() finds a semantic match in the graph, the existing node's attributes are merged with the new extraction using an overlay merge (apply_capped_attributes() in attribute_utils.py with merge_mode='overlay'), preserving prior values that the new extraction omitted. Edge resolution (resolve_extracted_edge() in edge_operations.py:638) works similarly but uses merge_mode='replace' for typed edge attributes. Edges accumulate provenance (episodes list) across extractions.
Summarization refines entity descriptions. extract_attributes_from_nodes() (node_operations.py:744) first calls per-entity attribute extraction via an LLM prompt, then batched summary generation via _extract_entity_summaries_batch(). Nodes with short summaries have edge facts appended directly (no LLM call); longer ones are partitioned into flights of 30 and sent to the LLM's extract_summaries_batch prompt. The per-entity summary cap is MAX_SUMMARY_CHARS (configurable in text_utils.py). Saga summarization (Graphiti.summarize_saga(), graphiti.py:499) incrementally updates a saga's running summary using a wall-clock watermark (last_summarized_at) and episode-time watermark (last_summarized_episode_valid_at), so backfilled episodes are caught on subsequent runs without re-summarizing everything.
Invalidation / forgetting uses bi-temporal markers. Edges carry valid_at (when a fact became true) and invalid_at (when it stopped being true). When resolve_extracted_edge() identifies contradictions, the older edge's invalid_at is set to the new edge's valid_at, and expired_at records when the system recognized the change. The resolve_edge_contradictions() function (edge_operations.py:547) handles the temporal algebra — an old edge is invalidated only if its validity window precedes the new edge's window. Nodes can be removed via remove_episode() (graphiti.py:1892), which cascade-deletes edges and nodes that were created solely by the removed episode. Whole groups are deletable via Node.delete_by_group_id(). There is no TTL-based automatic decay — forgetting is explicit through invalidation or deletion.
topoteretes/cognee
answeredUpdates use chunk-level incremental diffing (cognee/api/v1/update/update.py:28): new content is diffed against stored text via a paragraph-anchored multi-region diff, only changed chunks are re-chunked and re-extracted, unchanged chunks keep their ids, entities and embeddings. Falls back to full delete+re-add+cognify when preconditions fail. The document keeps its data_id across every path. Consolidation runs through improve() (cognee/api/v1/improve/improve.py:101) with nine ordered stages: feedback_weights, persist_session_qa, persist_agent_traces, extract_agent_context, distill_sessions, update_user_preferences, build_truth_subspace, triplet_enrichment, global_context_index (cognee/modules/improve/registry.py:27-37). Each stage gates first with zero LLM calls; improvements serialize via the improve lock. Deletion via forget() (cognee/api/v1/forget/forget.py:22) supports data_id, dataset, everything=True, and memory_only=True, deleting across graph, vector, and session backends with pipeline status reset for re-cognify. DataPoint.valid_to provides bi-temporal fact supersession.
supermemoryai/supermemory
answeredMemory lifecycle is tracked through version chains and state flags. Versioning: each MemoryEntry has version, isLatest, parentMemoryId, and rootMemoryId (apps/mcp/src/shared/types.ts:140-147). The history array (line 151) stores prior versions as MemoryEntryHistory objects (lines 125-137). Forgetting: the isForgotten boolean flag marks memories as forgotten (apps/mcp/src/shared/types.ts:143). The forgetMemory method (apps/mcp/src/server/client/index.ts:181-239) first tries exact-match deletion via the SDK, then falls back to a similarity search (threshold 0.85, line 200) to find a semantically matching memory to forget. TTL/expiration: forgetAfter (nullable string) and forgetReason (nullable string) fields on DocumentMemoryEntry (apps/mcp/src/shared/types.ts:81-82) suggest scheduled expiration, though the MCP server does not implement the expiry logic itself — that happens in the upstream API. Update/merge: the add_memory tool (apps/mcp/src/server/tools/add-memory.ts) always creates new memory; there is no merge or consolidation logic in this MCP server. The updatesMemoryId and nextVersionId fields on the graph type (apps/mcp/src/widget/views/Graph.tsx:99-100) confirm a linked-list version pattern. Decay and deletion are server-side concerns delegated to the hosted API.
MemoriLabs/Memori
answeredUpdates, consolidation, and deletion follow distinct patterns:
Incremental updates via upsert. Both entity facts and knowledge-graph triples use PostgreSQL's ON CONFLICT mechanism. For entity facts, the unique constraint is (entity_id, uniq), where uniq is a deterministic hash of the fact content (memori/storage/drivers/postgresql/_driver.py:257,278-280). When the same fact is re-extracted from a later conversation, the row is not duplicated — instead num_times is incremented and date_last_time is refreshed. The same applies to knowledge-graph triples (constraint on entity_id, subject_id, predicate_id, object_id, memori/storage/drivers/postgresql/_driver.py:572-574) and process attributes (constraint on process_id, uniq).
Conversation summaries. The AdvancedAugmentation response can include a conversation.summary string, which is written back to memori_conversation.summary via the conversation.update write path (memori/memory/augmentation/augmentations/memori/_augmentation.py:280-285). This summary is then used as context for future augmentations: when selecting messages to send to the API, the augmentation handler reads the existing summary and sends it alongside the latest user/assistant turn rather than the full history (memori/memory/augmentation/augmentations/memori/_augmentation.py:53-84,135-137).
Deletion. There is an explicit Recall.delete_entity_memories() method (memori/memory/recall.py:193-211) that deletes all facts and knowledge-graph entries for a given entity. In BYODB mode, the driver's delete_by_entity() cascades through the schema: deleting from memori_entity_fact (which cascades to memori_entity_fact_mention) and from memori_knowledge_graph (which cascades to orphaned subjects, predicates, and objects).
No TTL or decay. This version of the codebase does not implement automatic time-to-live, decay, or consolidation merging of facts. There is no background job scanning for stale memories. The recall_relevance_threshold (default 0.1) provides a static retrieval-time filter, but facts persist indefinitely once written. Individual scaffolding for versioning (the uuid and date_updated columns on every table) exists in the schema but is not leveraged for history tracking at the application layer.
MemTensor/MemOS
answeredVersioned archival + Dream consolidation + scheduler. NodeHandler.resolve() (handler.py:76-188) merges conflicting memories with MERGED_TO edge, sets originals to archived. Each TextualMemoryMetadata has version + history of ArchivedTextualMemory (item.py:109-128). GraphStructureReorganizer (reorganizer.py:81-107) runs background queue for add/remove/merge/update + periodic clustering under LLM-summarized parents. Scheduler has MEM_ARCHIVE_TASK_LABEL (task_schemas.py:38). Dream plugin (off by default): DreamSignalStore (signal_store.py:8-62) accumulates signals, triggers at 100, runs Contextualizer->MotiveFormation->DirectRecall->ConsolidationReasoning->DiarySummary->Persistence. DreamMemoryLifecycle (dream/types.py:67-80): last_hit_at, hit_count, usefulness_score, invalidated_by_feedback. maintenance.py (dream/maintenance.py:1-56) documents 3 planned strategies (not implemented): stale-by-TTL, low-usefulness archive, invalidation-by-feedback. Feedback API (mem_feedback/feedback.py:315) archives replaced memories.
plastic-labs/honcho
answeredMemory lifecycle has four phases. Creation: The Deriver extracts observations on message creation; each observation gets times_derived=1. On repeat writes, instead of inserting duplicates the times_derived counter is incremented (src/crud/document.py:693-695), strengthening the fact's reinforcement signal. Consolidation via Dreaming: The Dreamer (src/dreamer/orchestrator.py) runs scheduled 'dreams' using two specialist agents. The DeductionSpecialist (src/dreamer/specialists.py) reads existing explicit observations and produces deductive conclusions via a tool-using agent loop (tools: get_recent_observations, search_memory, search_messages, create_observations_deductive, delete_observations, update_peer_card). The InductionSpecialist produces inductive generalizations (patterns, preferences, personality) from both explicit and deductive conclusions. Surprisal-based prioritization (src/dreamer/surprisal.py) pre-filters observations: it builds a tree structure from embeddings, computes geometric surprisal scores ranking how anomalous or informative each observation is, and feeds the highest-surprisal observations as hints to the specialist agents — focusing the expensive dream cycle on the most 'interesting' or novel findings. Reasoning trees (src/dreamer/trees/) link each conclusion to its premises via document_sources table and source_ids references, enabling get_reasoning_chain traversal. Deletion and cleanup: Documents are soft-deleted via deleted_at timestamp (src/crud/document.py:901-948). The ReconcilerScheduler (src/reconciler/scheduler.py) periodically runs cleanup_soft_deleted_documents() which hard-deletes rows soft-deleted >5 minutes ago and removes their vectors from the external store. The reconciler also heals missed vector syncs and cleans stale queue items. Summarization: Parallel to memory extraction, the summarizer.py creates two-tier session summaries (short every ~20 messages, long every ~60) stored on the session metadata, providing a compressing lifecyle for long-running conversations.
MemMachine/MemMachine
answeredMemMemory manages memory lifecycle through three distinct mechanisms: update, consolidation, and eviction/forgetting. Updates are LLM-driven: when a new episode arrives, IngestionService._process_single_set() (semantic_ingestion.py:122) loads existing features for each category, then calls llm_feature_update() with both the old profile and new message. The LLM returns semantically-meaningful mutations as add/delete commands. Consolidation runs asynchronously for semantic memory when a tag accumulates >=20 features (the consolidation_threshold). _deduplicate_features() (semantic_ingestion.py:447) calls llm_consolidate_features() with all features grouped by tag, plus an LLM prompt that can merge, split, and delete redundant entries. The prompt instructs: "If enough memories share similar features... delete all of them and create a single new memory containing a list." The consolidation is a keep-list design — memories not in keep_memories are deleted. In short-term memory, consolidation is summarization (ShortTermMemoryConsolidator, short_term_memory.py:467): when the deque's total message length exceeds message_capacity (default 64000 chars), the oldest episodes are evicted and a background _run_summary_loop() incrementally generates a summary via LLM. The summary is prepended on retrieval. If the summary prompt exceeds the context window, episodes are binary-split recursively until they fit. Forgetting/deletion is explicit: episodes can be deleted by UID (delete_episodes()) in both short-term and long-term memory. Session-level drops (drop_session_partition()) on the event backend delete the entire VectorStore collection and SegmentStore partition. Semantic features can be deleted individually, by set+category filter, or by set_id. The background ingestion uses a backoff-retry loop (semantic_memory.py:783) that catches transient failures, and marks messages as ingested only after successful processing. There is no built-in TTL-based decay for episodic memory, and no explicit versioning/history for features — updates replace semantically, and the storage layer stores only the latest value per feature.
← How are memories retrieved and injected into the prompt? · How is memory scoped and isolated? →