Relevant source files
The following files were used as context for generating this wiki page:
- integrations/mem0-agent-plugin/core/memory_core.py
- mem0/memory/storage.py
- integrations/antigravity-plugin/core/memory_core.py
- integrations/agent-plugin-core/python/memory_core.py
- integrations/kimi-plugin/core/memory_core.py
- integrations/codex-plugin/core/memory_core.py
- integrations/cursor-plugin/core/memory_core.py
- integrations/claude-code-plugin/core/memory_core.py
- mem0/memory/main.py
- mem0-ts/src/oss/src/storage/SQLiteManager.ts
- mem0-ts/src/oss/src/storage/base.ts
- mem0-ts/src/oss/src/storage/MemoryHistoryManager.ts
- mem0-ts/src/oss/src/storage/SupabaseHistoryManager.ts
- mem0/memory/utils.py
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 Name | Column Name | Data Type | Key Constraints / Purpose |
|---|---|---|---|
history | id | TEXT | Primary Key, UUID string identifying the history event |
history | memory_id | TEXT | Identifier linking the mutation to a specific memory record |
history | old_memory | TEXT | Previous state value before an update or deletion |
history | new_memory | TEXT | Updated state value after a mutation |
history | event | TEXT | Category or type of mutation event |
history | created_at | DATETIME | Timestamp when the record was initially created |
history | updated_at | DATETIME | Timestamp when the record was last modified |
history | is_deleted | INTEGER | Deletion status flag (0 for active, 1 for deleted) |
history | actor_id | TEXT | Identifier of the entity or agent triggering the action |
history | role | TEXT | Role associated with the event or actor |
messages | id | TEXT | Primary Key, UUID string identifying the message |
messages | session_scope | TEXT | Scoping key grouping conversation turns by session |
messages | role | TEXT | Sender role (e.g., user, assistant, system) |
messages | content | TEXT | Raw textual body of the conversation turn |
messages | name | TEXT | Optional display name or handle for the message author |
messages | created_at | DATETIME | ISO 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")
returnSources: 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 Name | Data Type | Constraint / Role |
|---|---|---|
id | TEXT | Primary Key |
memory_id | TEXT | References target memory entry |
old_memory | TEXT | Previous state value |
new_memory | TEXT | Updated state value |
event | TEXT | Mutation category or event type |
created_at | DATETIME | Creation timestamp |
updated_at | DATETIME | Modification timestamp |
is_deleted | INTEGER | Deletion flag status |
actor_id | TEXT | Identifier of performing actor |
role | TEXT | Role 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 Name | Target Column | Purpose |
|---|---|---|
events | repo_id | Stores incremental prompt, tool, and session event payloads |
session_scopes | repo_id | Maintains persistent working directory and git root bindings per session |
flushes | repo_id | Tracks batch synchronization attempts, payload identifiers, and error statuses |
retrievals | repo_id | Logs context retrieval scores, ranks, and injected memory snippets |
operations | repo_id | Records execution duration, success flags, and error traces for agent tools |
sidekick_runs | repo_id | Markers for native subagent execution lifecycles, transcripts, and final messages |
Sources: integrations/mem0-agent-plugin/core/memory_core.py:1209-1216
Related
On This Page
OverviewSQLite Storage Architecture and InitializationConnection Setup and Thread IsolationSchema Generation and Table DefinitionsHistory Mutation Records and VersioningLogging State Mutations and Before-and-After ValuesHistory Retrieval MethodsSession Message Audit and StagingStoring Conversation Turns and Pruning Prerequisite MessagesRetrieving Bounded History and Session ContextsSchema Migrations and Table EvolutionHistory Table Schema Validation and TransformationExpected Schema DefinitionAgent Checkpoint and Flush IsolationTracking Inflight Flushes and Uncommitted Event IDsSubagent State Markers and Repository ScopesRelated