Disclosure: I'm Mohammed, founder of MemorySync. I drafted this article with AI assistance and reviewed it for accuracy against the official MemorySync and LangGraph docs.
If your LangGraph agent forgets everything the moment you start a new conversation, the graph is usually doing exactly what it was told. A checkpointer remembers a thread. It was never meant to remember a user. This post explains the split between the checkpointer and the store, then adds per-user memory that works across threads with MemorySync's LangGraph integration.
The LangGraph behaviour below comes from LangGraph's persistence docs and memory guide. Everything about MemorySync comes from its LangGraph Memory guide. The code is adapted from those docs. I haven't run it for this post, so check it against your versions.
Why the agent "forgets"
LangGraph has two persistence pieces, and they solve different problems:
| Checkpointer | Store | |
|---|---|---|
| Memory type | Short-term, scoped to one thread | Long-term, shared across threads |
| How you reach it | Pass a thread_id in the graph config |
Read and write items from nodes or app code |
| Typical contents | The conversation so far, interrupts, time travel | User preferences, facts, shared knowledge |
So two things commonly go wrong:
-
A new
thread_idmeans a fresh state. The checkpointer brings back the history ofthread-1when you callthread-1again. Callthread-2and it starts empty. That's by design. -
MemorySaver/InMemorySaverlive in RAM. LangGraph's docs say it directly: when the process restarts, every checkpoint is gone. For production they point you to a persistent checkpointer such asPostgresSaver.
Fixing (2) gets you threads that survive restarts. It doesn't fix (1). For "remember this user in every conversation" you need a store, and the store you get out of the box, InMemoryStore, also lives in memory.
The usual setup compiles the graph with both:
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore
graph = builder.compile(checkpointer=InMemorySaver(), store=InMemoryStore())
Keep the checkpointer. The rest of this post swaps out the store and adds per-user recall.
Where MemorySync fits
MemorySync's guide divides the work the same way: thread state (checkpoints, resume, time travel) stays with a LangGraph checkpointer like langgraph-checkpoint-postgres, -sqlite or -redis. Long-term memory across threads goes to MemorySync. The integration has five pieces:
-
MemorySyncStore: a LangGraphBaseStore, passed in withstore=. -
MemorySyncMemoryMiddleware: injects context before the model call forcreate_agent(langchain 1.x). -
create_memorysync_pre_model_hook: the same injection forcreate_react_agent. -
create_persist_turn_node: a graph node that sends the latest user message to MemorySync, where durable facts are extracted. -
create_memory_search_tool: a memory search tool the graph can call.
Step 1: Install
pip install "langchain-memorysync[langgraph]"
Set MEMORYSYNC_API_KEY in your environment, or pass the key in explicitly. The guide lists langgraph 1.2+ and Python 3.10+. On Node.js the same exports ship in memorysync-langchain under the /langgraph subpath, which needs @langchain/langgraph 0.2+ and Node 18+. The examples here are Python.
Step 2: Swap the store and namespace it per user
Namespaces are string tuples, the same as in LangGraph. Put the user id in the namespace:
from langchain_memorysync.langgraph import MemorySyncStore
store = MemorySyncStore() # reads MEMORYSYNC_API_KEY from the env
store.put(("memories", "user-1"), "diet", {"text": "vegetarian"})
item = store.get(("memories", "user-1"), "diet")
print(item.value) # {'text': 'vegetarian'}
hits = store.search(("memories", "user-1"), query="what do they eat?")
print(hits[0].value, hits[0].score)
According to the docs, each namespace maps to its own server-side scope, so ("memories", "user-a") and ("memories", "user-b") stay separate on the server rather than through a client-side filter. Two more things the guide notes:
-
No embedding config. LangGraph stores usually need an index config with an embedder and dimensions. MemorySync embeds stored values server-side, so
searchworks without one. - Retries don't duplicate. LangGraph re-runs nodes after interrupts and errors. A key holds one live value: an identical retry is recognised server-side, and a changed value replaces the old one.
Store values are treated as framework state. They don't show up in MemorySync's memory list, search or recall.
Step 3: Compile it into an agent
Tools reach the store through get_store():
from langchain.agents import create_agent
from langchain_core.tools import tool
from langgraph.config import get_store
from langchain_memorysync.langgraph import MemorySyncStore
@tool
def save_preference(key: str, value: str) -> str:
"""Save a user preference to long-term memory."""
get_store().put(("memories", "user-1"), key, {"text": value})
return f"Saved {key}."
@tool
def recall_preferences(query: str) -> str:
"""Search the user's long-term memory."""
hits = get_store().search(("memories", "user-1"), query=query)
return "\n".join(h.value["text"] for h in hits) or "Nothing stored yet."
agent = create_agent(
model,
tools=[save_preference, recall_preferences],
store=MemorySyncStore(),
)
In a real app, replace the hard-coded "user-1" with your own stable user id.
Step 4: Recall per user without tool calls
Explicit save and recall tools only work when the model decides to call them. If you want the user's context to be there every turn, inject it before the model call. Use the middleware for create_agent or the pre-model hook for create_react_agent:
# create_agent (langchain 1.x)
from langchain.agents import create_agent
from langchain_memorysync.langgraph import MemorySyncMemoryMiddleware
agent = create_agent(
model,
tools=tools,
middleware=[MemorySyncMemoryMiddleware(user_id="customer-7")],
)
# create_react_agent
from langgraph.prebuilt import create_react_agent
from langchain_memorysync.langgraph import create_memorysync_pre_model_hook
agent = create_react_agent(
model,
tools,
pre_model_hook=create_memorysync_pre_model_hook(user_id="customer-7"),
)
The docs say both only change what the model sees for that one call. Your checkpointed state isn't modified, so the stored transcript stays exactly what was said. If nothing relevant is found, the input passes through unchanged. Auto-persistence on the middleware is off by default.
Step 5: Save what the user tells you
To write facts from the conversation, add the persist node after your model node:
from langgraph.graph import StateGraph, MessagesState, START, END
from langchain_memorysync.langgraph import create_persist_turn_node
remember = create_persist_turn_node(user_id="customer-7")
graph = StateGraph(MessagesState)
graph.add_node("agent", call_model)
graph.add_node("remember", remember)
graph.add_edge(START, "agent")
graph.add_edge("agent", "remember")
graph.add_edge("remember", END)
app = graph.compile()
app.invoke({"messages": [("user", "I always fly aisle.")]},
config={"configurable": {"thread_id": "trip-1"}})
The session defaults to the graph's thread_id, so facts are grouped by conversation without extra wiring. If you're on create_agent, there's another option: MemorySyncMemoryMiddleware(user_id=..., auto_persist=True).
Step 6: Check it across two threads
This is the test the original question needed: two different thread_ids for the same user. It's adapted from the guide's complete example:
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_memorysync.langgraph import (
MemorySyncMemoryMiddleware,
MemorySyncStore,
create_memory_search_tool,
)
USER = "customer-7"
agent = create_agent(
ChatOpenAI(model="your-model"),
tools=[create_memory_search_tool(user_id=USER)],
middleware=[MemorySyncMemoryMiddleware(user_id=USER, auto_persist=True)],
store=MemorySyncStore(),
)
agent.invoke(
{"messages": [{"role": "user", "content": "I'm vegetarian and I fly aisle."}]},
config={"configurable": {"thread_id": "monday"}},
)
out = agent.invoke(
{"messages": [{"role": "user", "content": "Book my trip: flight plus a dinner spot."}]},
config={"configurable": {"thread_id": "friday"}},
)
print(out["messages"][-1].content)
The friday thread has no checkpoint history from monday. Any recall of the diet or seat preference has to come from the per-user memory, and the guide says that holds even if you stop the process between the two calls. If the second answer knows neither fact, look at user_id first. It has to be the same value in both calls.
The search tool is read-only and scoped to one end user. It returns readable strings instead of raising, because a tool exception would abort the whole graph run.
Recap
- Checkpointer = this thread. Keep it, and use a persistent one (Postgres, SQLite, Redis) in production.
- Store = across threads. Namespace it by user.
- To make the agent remember the user every turn, inject context (middleware or pre-model hook) and persist facts (persist node or
auto_persist=True).
Try it
If you want to see the save, reload and recall loop before wiring it into a graph, the MemorySync playground runs it in your browser: memorysync.io/try.
Top comments (0)