# Persistent memory for a LangChain chatbot after LangChain 1.0

*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](https://reference.langchain.com/python/langchain-classic/memory/buffer/ConversationBufferMemory), [v1 migration guide](https://docs.langchain.com/oss/python/migrate/langchain-v1)).

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](https://reference.langchain.com/python/langchain-classic/chains/conversation/base/ConversationChain)). 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:

1. **The transcript.** The ordered messages of one thread. You need it so the model can follow "yes, do that" three turns later.
2. **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](https://docs.memorysync.io/guides/langchain?utm_source=hashnode&utm_medium=social&utm_campaign=pivot_oct19&utm_content=langchain-persistent-memory-memorysync) 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

```bash
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:

```python
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_id` is the conversation thread. Reads and `clear()` only touch that thread.
- `user_id` is 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_messages` caps how many trailing messages a read returns, so a long thread doesn't blow your prompt budget. Writes aren't affected.
- `read_only` blocks writes and `clear()`, 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:

```python
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.

```python
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](https://docs.memorysync.io/guides/langchain-tools?utm_source=hashnode&utm_medium=social&utm_campaign=pivot_oct19&utm_content=langchain-persistent-memory-memorysync) adds five structured tools: `add_memory`, `search_memory`, `list_memories`, `update_memory` and `delete_memory`.

```python
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_memory` derives an idempotency key from the content, so repeating "remember X" returns "already stored" instead of a duplicate.
- Memory text can't be edited. `update_memory` changes tags and importance, and replacing a fact is delete plus add.
- `end_user_id` scopes every call to that user. Pass `read_only=True` if 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](https://memorysync.io/try?utm_source=hashnode&utm_medium=social&utm_campaign=pivot_oct19&utm_content=langchain-persistent-memory-memorysync).

