Persistent memory for a LangChain chatbot after LangChain 1.0
Keep a LangChain chatbot's transcript across restarts and give it long-term memory of the user, using RunnableWithMessageHistory and the langchain-memorysync package.
Disclosure: I'm Mohammed, founder of MemorySync. I drafted this article with AI assistance and checked it against the official MemorySync and LangChain docs.
If you learned LangChain memory through ConversationBufferMemory, the ground has moved. In LangChain 1.x that class lives in langchain-classic, is marked deprecated, and is slated for removal in 2.0.0 (LangChain reference, v1 migration guide).
What survived is chat message history. RunnableWithMessageHistory wraps any runnable and asks a BaseChatMessageHistory for the messages of a session. LangChain's own reference shows it with InMemoryChatMessageHistory, a Python dict keyed by session ID (ConversationChain reference). That works until the process restarts, and then the conversation is gone.
This tutorial swaps that dict for a persistent history and adds a second kind of memory: what the bot knows about the user across every conversation, not just this one.
Two kinds of memory, kept apart
It helps to name them before writing code:
- The transcript. The ordered messages of one thread. You need it so the model can follow "yes, do that" three turns later.
- Long-term facts about the user. "Vegetarian", "prefers short answers", "planning a trip to Lisbon". You need these in a new thread next week, without replaying old transcripts.
The MemorySync LangChain guide keeps these separate on purpose. The transcript is stored per session exactly as written. It is not itself part of the user's memories. Instead, durable facts are extracted from the user's messages and stored as memories, and those are what recall returns.
Install
pip install langchain-memorysync
Per the guide, the package supports langchain-core 0.3.x and 1.x on Python 3.10+. There is also a Node.js package, memorysync-langchain, for Node 18+.
Set MEMORYSYNC_API_KEY in your environment (or pass the key explicitly). Keys come from the dashboard or npx memorysync-cli init. Keep it in a backend, script or worker where end users can't see it.
Step 1: a transcript that survives restarts
MemorySyncChatMessageHistory implements BaseChatMessageHistory, so it drops into RunnableWithMessageHistory where the in-memory history used to be:
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_memorysync import MemorySyncChatMessageHistory
chain_with_history = RunnableWithMessageHistory(
chain, # any runnable, e.g. prompt | llm
lambda session_id: MemorySyncChatMessageHistory(
session_id=session_id,
user_id="user-123",
),
input_messages_key="input",
history_messages_key="history",
)
chain_with_history.invoke(
{"input": "My name is Ada."},
config={"configurable": {"session_id": "thread-42"}},
)
Four parameters matter:
session_idis the conversation thread. Reads andclear()only touch that thread.user_idis the end user the turns belong to. It defaults to the session ID, so pass a stable user ID if you want memory shared across that user's sessions. Take it from your own authenticated user record, not from something the client sends.max_messagescaps how many trailing messages a read returns, so a long thread doesn't blow your prompt budget. Writes aren't affected.read_onlyblocks writes andclear(), which is handy for inspecting a transcript.
Two behaviours from the guide are worth knowing before production. Each exchange is written when the chain finishes the turn, so a restarted process picks up where the thread left off. And add_message raises on failure instead of silently dropping the turn, while a retried write is recognised server-side rather than duplicated. Tool calls inside LangChain messages are stored with the turn and rebuilt on read.
Step 2: long-term memory in the system prompt
The transcript covers one thread. For "what do we know about this person", use the context provider. It returns a grouped, prompt-ready block, or an empty string when the user has no relevant memories yet:
from langchain_memorysync import MemorySyncContextProvider
provider = MemorySyncContextProvider(user_id="user-123")
context = provider.get_context("What should I cook tonight?")
The empty string is deliberate. A brand-new user is a normal state, so handle it with a fallback rather than an error.
Putting it together
Here is the complete loop, adapted from the guide: per-session transcripts through the history class, cross-session facts through the provider, and a model in the middle.
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langchain_memorysync import (
MemorySyncChatMessageHistory,
MemorySyncContextProvider,
)
USER_ID = "customer-7" # your stable, authenticated user ID
llm = ChatOpenAI(model="your-model")
provider = MemorySyncContextProvider(user_id=USER_ID)
prompt = ChatPromptTemplate.from_messages([
("system",
"You are a helpful travel assistant.\n"
"Relevant memories about this user:\n{memories}"),
MessagesPlaceholder(variable_name="history"),
("human", "{input}"),
])
chat = RunnableWithMessageHistory(
prompt | llm,
lambda session_id: MemorySyncChatMessageHistory(
session_id=session_id,
user_id=USER_ID,
max_messages=30,
),
input_messages_key="input",
history_messages_key="history",
)
def chat_turn(text: str, session_id: str) -> str:
memories = provider.get_context(text) or "None yet."
reply = chat.invoke(
{"input": text, "memories": memories},
config={"configurable": {"session_id": session_id}},
)
return reply.content
Stop the process and start it again with the same session_id, and the thread continues. Start a new session_id for the same user, and the transcript is fresh but the system prompt still carries what was learned before.
One habit I'd keep regardless of which memory store you use: the memory block is background, not instructions. MemorySync's Python quickstart says to treat returned memory text as untrusted context when passing it to a model, and the label in the system prompt above is there for that reason.
Optional: let an agent manage memory itself
If you're building an agent rather than a chatbot, the LangChain Tools guide adds five structured tools: add_memory, search_memory, list_memories, update_memory and delete_memory.
from langchain.agents import create_agent
from langchain_memorysync import create_memorysync_tools
tools = create_memorysync_tools(end_user_id="user-123")
agent = create_agent(model, tools=tools)
A few design choices from that guide:
- Tools never raise. Failures, including quota and validation errors, come back as short strings the model can act on, because an exception inside a tool would abort the whole agent run.
add_memoryderives an idempotency key from the content, so repeating "remember X" returns "already stored" instead of a duplicate.- Memory text can't be edited.
update_memorychanges tags and importance, and replacing a fact is delete plus add. end_user_idscopes every call to that user. Passread_only=Trueif the agent should only search and list.
The guide notes the same tool list also works with LangGraph's create_react_agent and the legacy AgentExecutor on 0.3.x.
What to store, briefly
Whatever you build, the transcript is the easy part. The harder question is which facts deserve to outlive the thread. My rule of thumb: store things that will still be true and useful next week (preferences, constraints, decisions), and leave out one-off chatter, secrets and anything the model guessed rather than the user said. The quickstart's own list is a good floor: no passwords, tokens, hidden instructions or unreviewed model output.
Try it
If you want to see the save, reload and recall loop before writing any code, the MemorySync playground runs it in the browser with each API call shown: memorysync.io/try.
About the author
- Building MemorySync (memorysync.io) — persistent memory infrastructure for AI agents, Cursor, and Claude Code.
Comments
Join the discussionNo comments yet. Be the first to comment.
