The short version: Portability is an architecture decision you make on day one, not a migration you do in a crisis. Put your memory behind a thin add/all/snapshot interface, self-host the store, and periodically dump every memory to structured JSON you own. Three calls prove it works: add, get_all, and re-add into a fresh backend.

On July 15, 2026, ByteDance's Doubao and Alibaba's Qwen switch off their companion agents to comply with China's new anthropomorphic-interaction rules — the largest forced deletion of agent memory in history. Millions of agents' accumulated context evaporates, and for Qwen there is no export button at all. That is the whole argument for this piece: if you build on a managed memory layer, you must be able to get every memory out as structured data before someone else decides you can't.

Why memory is the risky dependency#

RAG re-fetches documents on every turn; agent memory accumulates — it's the one asset that grows more valuable and more irreplaceable the longer your product runs. (If that distinction is fuzzy, see agent memory vs RAG.) You can rebuild an index from source files. You cannot rebuild three years of a user's preferences, corrections, and history from nothing. So the memory layer is exactly the dependency you least want to be unable to leave.

You can re-index documents any day of the week. You cannot re-derive a user's accumulated context — which is why the memory layer is the one place lock-in actually hurts.

The worked example: mem0, self-hosted#

mem0 is the most-adopted open-source memory layer (60K+ stars, Apache-2.0). The key move is choosing the open-source Memory() class over the hosted MemoryClient(), because Memory() runs against vector stores and LLMs you configure.

pip install mem0ai   # v2.0.12, verified Jul 13 2026

1. Add memories

from mem0 import Memory

m = Memory()  # self-hosted: vectors + metadata live in your store

m.add(
    [{"role": "user", "content": "I'm vegetarian and allergic to peanuts."}],
    user_id="rosa",
)
m.add("Prefers morning standups.", user_id="rosa")

add() returns a dict with a "results" key describing what was extracted. To keep the store in infrastructure you operate, build it from config instead:

config = {
    "vector_store": {"provider": "pgvector",
                     "config": {"connection_string": "postgresql://..."}},
    "llm": {"provider": "openai", "config": {"model": "gpt-4o-mini"}},
}
m = Memory.from_config(config)

Now the data sits in your Postgres. (For the tradeoffs of vector vs. filesystem stores, see filesystem vs vector database for agent memory.)

2. Export everything to JSON you own

This is the payoff. get_all() does a raw retrieval — no ranking, no semantic search — of every memory for a scope:

import json

dump = m.get_all(user_id="rosa")   # -> {"results": [ ... ]}
memories = dump["results"]         # each: id, memory, metadata, created_at

with open("rosa-memories.json", "w") as f:
    json.dump(memories, f, indent=2, default=str)

print(f"Exported {len(memories)} memories you now own.")

Note the shape: current mem0 wraps results in a "results" key (older 1.0-style responses returned a bare list, so normalize if you pin an old version). Each item carries id, the extracted memory text, metadata, and created_at. That JSON is now yours — commit it, ship it to S3, back it up. The hosted platform's create_memory_export()/get_memory_export() also exists, but those exports expire after 7 days, which tells you exactly how much the vendor expects you to keep.

3. Re-import into a fresh store to prove portability

Portability isn't real until you've reversed it. Stand up a brand-new backend and replay the JSON:

fresh = Memory()  # different backend, same interface

with open("rosa-memories.json") as f:
    memories = json.load(f)

for item in memories:
    fresh.add(
        item["memory"],
        user_id="rosa",
        metadata=item.get("metadata") or {},
        infer=False,   # store verbatim; don't re-extract
    )

infer=False writes each memory as-is instead of running LLM fact-extraction again, so you get a faithful copy rather than a re-interpretation. The round-trip — addget_alladd — is the entire proof that no single vendor holds your context hostage.

4. The thin interface that makes the backend swappable

Do steps 1–3 through a protocol, and mem0 becomes an implementation detail:

from typing import Protocol

class MemoryStore(Protocol):
    def add(self, text: str, user_id: str, metadata: dict | None = None) -> None: ...
    def all(self, user_id: str) -> list[dict]: ...
    def snapshot(self, user_id: str) -> list[dict]: ...
class Mem0Store:
    def __init__(self, config: dict | None = None):
        self._m = Memory.from_config(config) if config else Memory()

    def add(self, text, user_id, metadata=None):
        self._m.add(text, user_id=user_id, metadata=metadata or {})

    def all(self, user_id):
        return self._m.get_all(user_id=user_id)["results"]

    def snapshot(self, user_id):
        rows = self.all(user_id)
        # write rows to storage YOU control: Postgres, S3, a file
        return rows

Swap in a ZepStore or LettaStore later and nothing above the interface changes.

The same principle in Zep and Letta#

Zep's open-source Graphiti engine keeps a temporal knowledge graph in your own Neo4j, FalkorDB, or Kuzu instance; each edge is a typed fact with a valid_from/valid_until window, so your export is graph nodes and edges rather than flat strings. Letta (Apache-2.0, self-hostable via Docker) models memory as editable OS-style blocks labeled human, persona, and so on; you read them straight out with client.agents.blocks.list(agent_id=...) or client.agents.blocks.retrieve(agent_id=..., block_label="human").value. Different shapes, same requirement: a self-hostable store and a documented read-out path. For a fuller comparison, see mem0 vs Zep vs Letta and the three places to keep an agent's memory.

Day one, not crisis day#

The two-part rule that survives any vendor decision: (1) access memory only through a thin interface so the backend is swappable, and (2) run a scheduled snapshot() that writes structured JSON to storage you control. Do both on the first day and a forced shutdown is a re-import job, not a wake. The teams whose agents went dark this week didn't lack a migration plan — they lacked an export they'd already run. Don't be them.