Skip to main content
doc0blade47/mem0documentation home
Sign in

Storage and History

Last updated Oct 11, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

The persistence and audit layer manages local relational storage across the Python core, TypeScript OSS package, and agent plugins. It initializes SQLite databases, enforces history and message table schemas, and provides structured transaction tracking for memory versioning and conversational audits.

Sources: mem0/memory/storage.py:11-19, mem0-ts/src/oss/src/storage/SQLiteManager.ts:6-15

By recording state mutation logs, before-and-after values, and session scopes, the storage engine supports transactional rollback, incremental schema migrations, and reliable agent checkpointing across distributed plugin environments.

Sources: mem0/memory/storage.py:20-101, integrations/agent-plugin-core/python/memory_core.py:699-714

SQLite Storage Architecture and Initialization

Connection Setup and Thread Isolation

The SQLiteManager class initializes a SQLite connection using the database path specified by db_path, which defaults to ":memory:" if omitted. To support multi-threaded environments and asynchronous workers across client runtimes, check_same_thread=False is passed to sqlite3.connect(). A thread lock (threading.Lock) protects all transaction boundaries, schema operations, and query executions.

Sources: mem0/memory/storage.py:11-19

Important

When check_same_thread=False is enabled, concurrent access from multiple threads must be serialized explicitly. SQLiteManager wraps every state change, table creation, and migration step inside self._lock to prevent race conditions during transaction commits.

Sources: mem0/memory/storage.py:14-19

Schema Generation and Table Definitions

Upon instantiation, SQLiteManager invokes schema checks and table creation methods to guarantee that the required relational structures exist. It runs _migrate_history_table(), _create_history_table(), and _create_messages_table(), each wrapped in explicit BEGIN, COMMIT, and ROLLBACK blocks to ensure atomicity.

Sources: mem0/memory/storage.py:16-18

Table NameColumn NameData TypeKey Constraints / Purpose
historyidTEXTPrimary Key, UUID string identifying the history event
historymemory_idTEXTIdentifier linking the mutation to a specific memory record
historyold_memoryTEXTPrevious state value before an update or deletion
historynew_memoryTEXTUpdated state value after a mutation
historyeventTEXTCategory or type of mutation event
historycreated_atDATETIMETimestamp when the record was initially created
historyupdated_atDATETIMETimestamp when the record was last modified
historyis_deletedINTEGERDeletion status flag (0 for active, 1 for deleted)
historyactor_idTEXTIdentifier of the entity or agent triggering the action
historyroleTEXTRole associated with the event or actor
messagesidTEXTPrimary Key, UUID string identifying the message
messagessession_scopeTEXTScoping key grouping conversation turns by session
messagesroleTEXTSender role (e.g., user, assistant, system)
messagescontentTEXTRaw textual body of the conversation turn
messagesnameTEXTOptional display name or handle for the message author
messagescreated_atDATETIMEISO timestamp recorded when the message was stored

Sources: mem0/memory/storage.py:68-79, mem0/memory/storage.py:134-141

History Mutation Records and Versioning

Logging State Mutations and Before-and-After Values

The SQLiteManager class records state transitions via add_history and batch_add_history methods. Each mutation captures the state prior to modification in old_memory and the subsequent state in new_memory, tied together by a unique memory_id and tracked under an event category. Transaction execution is safeguarded by an explicit thread lock, rolling back changes on any encountered exception.

def add_history(
    self,
    memory_id: str,
    old_memory: Optional[str],
    new_memory: Optional[str],
    event: str,
    *,
    created_at: Optional[str] = None,
    updated_at: Optional[str] = None,
    is_deleted: int = 0,
    actor_id: Optional[str] = None,
    role: Optional[str] = None,
) -> None:
    with self._lock:
        try:
            self.connection.execute("BEGIN")
            self.connection.execute(
                """
                INSERT INTO history (
                    id, memory_id, old_memory, new_memory, event,
                    created_at, updated_at, is_deleted, actor_id, role
                )
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
                (
                    str(uuid.uuid4()),
                    memory_id,
                    old_memory,
                    new_memory,
                    event,
                    created_at,
                    updated_at,
                    is_deleted,
                    actor_id,
                    role,
                ),
            )
            self.connection.execute("COMMIT")
        except Exception as e:
            self.connection.execute("ROLLBACK")
            logger.error(f"Failed to add history record: {e}")
            raise
 
def batch_add_history(self, records: List[Dict[str, Any]]) -> None:
    with self._lock:
        try:
            self.connection.execute("BEGIN")
            self.connection.executemany(
                """
                INSERT INTO history (
                    id, memory_id, old_memory, new_memory, event,
                    created_at, updated_at, is_deleted, actor_id, role
                )
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
                [
                    (
                        str(uuid.uuid4()),
                        record.get("memory_id"),
                        record.get("old_memory"),
                        record.get("new_memory"),
                        record.get("event"),
                        record.get("created_at"),
                        record.get("updated_at"),
                        record.get("is_deleted", 0),
                        record.get("actor_id"),
                        record.get("role"),
                    )
                    for record in records
                ],
            )
            self.connection.execute("COMMIT")
        except Exception as e:
            self.connection.execute("ROLLBACK")
            logger.error(f"Failed to batch add history records: {e}")
            raise
 

Sources: mem0/memory/storage.py:150-226

Note

When batch inserting mutation records through batch_add_history, each dictionary entry defaults is_deleted to 0 if not explicitly supplied, while optional fields such as actor_id, role, created_at, and updated_at are safely retrieved using .get().

Sources: mem0/memory/storage.py:193-220

History Retrieval Methods

Version history for any tracked memory entry can be queried using the get_history method, which fetches all associated mutation records filtered by memory_id. The returned rows are ordered chronologically by their creation timestamp and updated datetime values, then mapped into dictionaries with boolean conversion applied to the is_deleted status flag.

def get_history(self, memory_id: str) -> List[Dict[str, Any]]:
    with self._lock:
        cur = self.connection.execute(
            """
            SELECT id, memory_id, old_memory, new_memory, event,
                   created_at, updated_at, is_deleted, actor_id, role
            FROM history
            WHERE memory_id = ?
            ORDER BY created_at ASC, DATETIME(updated_at) ASC
        """,
            (memory_id,),
        )
        rows = cur.fetchall()
 
    return [
        {
            "id": r[0],
            "memory_id": r[1],
            "old_memory": r[2],
            "new_memory": r[3],
            "event": r[4],
            "created_at": r[5],
            "updated_at": r[6],
            "is_deleted": bool(r[7]),
            "actor_id": r[8],
            "role": r[9],
        }
        for r in rows
    ]

Sources: mem0/memory/storage.py:227-255

Important

The sort order in get_history relies on ORDER BY created_at ASC, DATETIME(updated_at) ASC to preserve the exact linear trajectory of edits performed on a given memory item over its lifecycle.

Sources: mem0/memory/storage.py:229-236

Session Message Audit and Staging

Storing Conversation Turns and Pruning Prerequisite Messages

The SQLiteManager class records raw conversation turns in the messages table via the save_messages method. When messages are ingested under a given session_scope, each item is assigned a generated UUID, its respective role, content, and optional author name, and an ISO-formatted UTC timestamp.

def save_messages(self, messages: List[Dict[str, Any]], session_scope: str) -> None:
    if not messages:
        return
    with self._lock:
        try:
            self.connection.execute("BEGIN")
            now = datetime.now(timezone.utc).isoformat()
            for message in messages:
                self.connection.execute(
                    """
                    INSERT INTO messages (id, session_scope, role, content, name, created_at)
                    VALUES (?, ?, ?, ?, ?, ?)
                """,
                    (
                        str(uuid.uuid4()),
                        session_scope,
                        message.get("role"),
                        message.get("content"),
                        message.get("name"),
                        now,
                    ),
                )

Sources: mem0/memory/storage.py:257-278

Warning

Immediately after saving new rows, save_messages executes an automated retention pruning query that deletes any message for that session_scope exceeding the ten most recent entries, determined by descending created_at ordering.

Sources: mem0/memory/storage.py:279-291

Tip

The subquery within the pruning delete statement wraps the LIMIT 10 selection inside a derived table. This forces SQLite to materialize the ordered temporary result set before evaluating the outer NOT IN clause against the table.

Sources: mem0/memory/storage.py:282-289

Retrieving Bounded History and Session Contexts

Recent conversation turns are queried using the get_last_messages method, which accepts a session_scope identifier and an optional limit parameter defaulting to 10. The underlying SQL query fetches the latest records in descending order of creation, then wraps them in a subquery to re-sort the results chronologically in ascending order before returning them to the caller.

def get_last_messages(self, session_scope: str, limit: int = 10) -> List[Dict[str, Any]]:
    with self._lock:
        # Subquery picks the latest N rows (DESC + LIMIT), outer query
        # re-sorts them chronologically (ASC) for the caller.
        cur = self.connection.execute(
            """
            SELECT role, content, name, created_at FROM (
                SELECT role, content, name, created_at
                FROM messages
                WHERE session_scope = ?
                ORDER BY created_at DESC
                LIMIT ?
            ) ORDER BY created_at ASC
        """,
            (session_scope, limit),
        )
        rows = cur.fetchall()
 
    return [
        {
            "role": r[0],
            "content": r[1],
            "name": r[2],
            "created_at": r[3],
        }
        for r in rows
    ]

Sources: mem0/memory/storage.py:298-324

Schema Migrations and Table Evolution

History Table Schema Validation and Transformation

The SQLiteManager class implements an automated migration routine inside _migrate_history_table to handle evolution of the history table schema across storage revisions. When initialized, the manager inspects SQLite metadata tables to determine whether a pre-existing history table exists. If present, it queries column information via PRAGMA table_info(history) to evaluate whether the existing column set matches the expected production schema.

cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='history'")
if cur.fetchone() is None:
    self.connection.execute("COMMIT")
    return  # nothing to migrate
 
cur.execute("PRAGMA table_info(history)")
old_cols = {row[1] for row in cur.fetchall()}
 
expected_cols = {
    "id",
    "memory_id",
    "old_memory",
    "new_memory",
    "event",
    "created_at",
    "updated_at",
    "is_deleted",
    "actor_id",
    "role",
}
 
if old_cols == expected_cols:
    self.connection.execute("COMMIT")
    return

Sources: mem0/memory/storage.py:32-55

If column discrepancies are detected—such as legacy group-chat metadata columns—the migration procedure executes a safe table alteration wrapped inside an explicit transaction block. It drops any leftover staging tables from previous interrupted migrations, renames the active history table to history_old, provisions a new history table conforming to the current schema definition, copies intersecting columns, and drops the old table.

logger.info("Migrating history table to new schema (no convo columns).")
 
# Clean up any existing history_old table from previous failed migration
cur.execute("DROP TABLE IF EXISTS history_old")
 
# Rename the current history table
cur.execute("ALTER TABLE history RENAME TO history_old")
 
# Create the new history table with updated schema
cur.execute(
    """
    CREATE TABLE history (
        id           TEXT PRIMARY KEY,
        memory_id    TEXT,
        old_memory   TEXT,
        new_memory   TEXT,
        event        TEXT,
        created_at   DATETIME,
        updated_at   DATETIME,
        is_deleted   INTEGER,
        actor_id     TEXT,
        role         TEXT
    )
"""
)
 
# Copy data from old table to new table
intersecting = list(expected_cols & old_cols)
if intersecting:
    cols_csv = ", ".join(intersecting)
    cur.execute(f"INSERT INTO history ({cols_csv}) SELECT {cols_csv} FROM history_old")
 
# Drop the old table
cur.execute("DROP TABLE history_old")
 
# Commit the transaction
self.connection.execute("COMMIT")
logger.info("History table migration completed successfully.")

Sources: mem0/memory/storage.py:57-94

Warning

If any exception occurs during the schema transformation, the transaction block immediately invokes ROLLBACK and propagates the error, preventing partial table migrations or data corruption.

Sources: mem0/memory/storage.py:96-100

Expected Schema Definition

The history migration and table initialization logic enforces a strict ten-column schema structure. The table attributes, data types, and constraints used during _migrate_history_table and _create_history_table are outlined below.

Column NameData TypeConstraint / Role
idTEXTPrimary Key
memory_idTEXTReferences target memory entry
old_memoryTEXTPrevious state value
new_memoryTEXTUpdated state value
eventTEXTMutation category or event type
created_atDATETIMECreation timestamp
updated_atDATETIMEModification timestamp
is_deletedINTEGERDeletion flag status
actor_idTEXTIdentifier of performing actor
roleTEXTRole associated with the record

Sources: mem0/memory/storage.py:40-51, mem0/memory/storage.py:68-79, mem0/memory/storage.py:108-119

Agent Checkpoint and Flush Isolation

Tracking Inflight Flushes and Uncommitted Event IDs

The plugin memory cores manage synchronization through explicit database schema cleanups and state tracking. During database initialization or cleanup sequences, legacy or deprecated staging tables—specifically sidekick_calls, sidekick_snapshots, sidekick_state, and sidekick_packets—are unconditionally dropped from the SQLite connection to prevent schema pollution across revisions.

DROP TABLE IF EXISTS sidekick_calls;
DROP TABLE IF EXISTS sidekick_snapshots;
DROP TABLE IF EXISTS sidekick_state;
DROP TABLE IF EXISTS sidekick_packets;

Sources: integrations/mem0-agent-plugin/core/memory_core.py:693-696

To handle ongoing persistence tasks, the prepare_flush method utilizes an immediate transaction (BEGIN IMMEDIATE) to inspect pending memory flush packets for a given repository and session. It queries the flushes table for rows where the status is outside terminal states, enforcing retry thresholds.

def prepare_flush(
    self, repo: RepoContext, session_id: str, reason: str
) -> tuple[str, list[dict[str, Any]]] | None:
    with self.conn:
        self.conn.execute("BEGIN IMMEDIATE")
        existing = self.conn.execute(
            """SELECT * FROM flushes
               WHERE repo_id = ? AND session_id = ?
                 AND status NOT IN ('semantic-succeeded', 'explicitly-stored', 'gave-up')
               ORDER BY created_at LIMIT 1""",
            (repo.identity, session_id),
        ).fetchone()

Sources: integrations/mem0-agent-plugin/core/memory_core.py:797-808

Caution

If a flush packet exceeds MAX_FLUSH_ATTEMPTS (fixed at 5), the engine transitions its status to gave-up and records a telemetry event, preventing infinite retry loops on failing synchronization payloads.

Sources: integrations/mem0-agent-plugin/core/memory_core.py:65-70, integrations/mem0-agent-plugin/core/memory_core.py:809-816

Subagent State Markers and Repository Scopes

Subagent and sidekick instances are tracked via the sidekick_runs table, capturing runtime execution lifecycle events. The stop_subagent method manages concurrent subagent termination by wrapping operations in an immediate transaction block, resolving missing agent identifiers, and upserting execution records with transcript paths and final messages.

def stop_subagent(
    self,
    repo: RepoContext,
    session_id: str,
    agent_id: str,
    agent_type: str,
    transcript_path: str,
    final_message: str,
) -> str:
    now = utc_now()
    self.conn.execute("BEGIN IMMEDIATE")

Sources: integrations/mem0-agent-plugin/core/memory_core.py:1068-1078

self.conn.execute(
    """INSERT INTO sidekick_runs
       (repo_id, session_id, agent_id, agent_type, started_at,
        stopped_at, transcript_path, final_message)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?)
       ON CONFLICT(repo_id, session_id, agent_id) DO UPDATE SET
         stopped_at = excluded.stopped_at,
         transcript_path = excluded.transcript_path,
         final_message = excluded.final_message""",
    (
        repo.identity,
        session_id,
        agent_id,
        agent_type,
        now,
        now,
        bounded(transcript_path, 2000),
        redact(final_message).strip(),
    ),
)

Sources: integrations/mem0-agent-plugin/core/memory_core.py:1089-1108

Note

When an agent_id is omitted during subagent stoppage, the store queries for active records without a stop timestamp (stopped_at IS NULL). If exactly one overlapping run exists, it correlates automatically; otherwise, it assigns a unique fallback identifier using nanosecond timestamps.

Sources: integrations/mem0-agent-plugin/core/memory_core.py:1080-1088

Project context isolation is maintained by the repo_for_session method and the session_scopes table. Working directories and repository identities are pinned per session ID to guarantee that subsequent hooks within the same coding agent session reference consistent git metadata and root paths.

Table NameTarget ColumnPurpose
eventsrepo_idStores incremental prompt, tool, and session event payloads
session_scopesrepo_idMaintains persistent working directory and git root bindings per session
flushesrepo_idTracks batch synchronization attempts, payload identifiers, and error statuses
retrievalsrepo_idLogs context retrieval scores, ranks, and injected memory snippets
operationsrepo_idRecords execution duration, success flags, and error traces for agent tools
sidekick_runsrepo_idMarkers for native subagent execution lifecycles, transcripts, and final messages

Sources: integrations/mem0-agent-plugin/core/memory_core.py:1209-1216

Was this page helpful?