DEV Community

Mohammed Rafay
Mohammed Rafay

Posted on Originally published at memorysync.hashnode.dev

LangGraph SummarizationMiddleware wiped my chat history: keeping the full transcript with PostgresSaver

Disclosure: I work on MemorySync (I'm Mohammed, the founder). I drafted this page with AI assistance and checked it against the source thread and the official docs listed under Sources on Oct 28, 2026. I read the thread and the LangChain docs; I didn't run this agent for this page.

The question

In the LangChain forum thread "LangGraph + PostgreSQL: Chat history and summarization best practice" (Apr 20, 2026), a developer building a ChatGPT-style chatbot on LangGraph, with PostgreSQL as the checkpointer, described the bind:

When I summarize older messages and delete them from the database, the UI breaks because full chat history is lost

If I don't delete messages, context becomes too large for the model

They wanted the UI to show the full conversation while the model sees "summarized older messages" plus "recent messages".

Status (checked Oct 28, 2026): the thread is open, with one reply from a community member and no accepted answer. That reply's advice: keep your own messages table for the UI, and treat the checkpointer as the model's working context only.

Skip the memory plumbing around this fix. Once the fix below is in, the MemorySync LangGraph guide shows how to add per-user memory with MemorySync. MemorySync doesn't fix this error itself. When you're ready, see MemorySync. Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).

Why it happens

SummarizationMiddleware doesn't just shorten the prompt. LangChain's context engineering guide says summarization "persistently updates state - permanently replacing old messages with a summary that's saved for all future turns", and "future turns will see the summary instead of the original messages."

The checkpointer saves that state per thread. So after summarization runs, the thread's messages in state are the summary plus the recent messages it kept. If your UI renders the conversation from the agent's state, the older messages are gone from the screen. The trim example in the short-term memory docs does the same thing: its @before_model hook returns RemoveMessage(id=REMOVE_ALL_MESSAGES), which rewrites state too.

Fix 1: checkpointer for the model, your own table for the UI

This is the forum reply's approach. Let summarization manage the checkpointer, and write every turn to an append-only table you own. The UI reads only that table.

Agent setup, following the Postgres example in the short-term memory docs:

from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
from langgraph.checkpoint.postgres import PostgresSaver

DB_URI = "postgresql://user:password@localhost:5432/app"

with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
    checkpointer.setup()  # first run: creates LangGraph's tables
    agent = create_agent(
        model="your-model",
        tools=[...],
        middleware=[
            SummarizationMiddleware(
                model="your-summary-model",
                trigger=("tokens", 4000),
                keep=("messages", 20),
            )
        ],
        checkpointer=checkpointer,
    )
    # serve requests inside this block
Enter fullscreen mode Exit fullscreen mode

The thresholds are the docs' example values; pick ones that suit your model.

Your table, separate from LangGraph's:

CREATE TABLE chat_messages (
    id          BIGSERIAL PRIMARY KEY,
    thread_id   TEXT NOT NULL,
    role        TEXT NOT NULL,
    content     TEXT NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ON chat_messages (thread_id, id);
Enter fullscreen mode Exit fullscreen mode

Write both sides of each turn (conn is a psycopg connection):

def save_message(conn, thread_id: str, role: str, content: str) -> None:
    conn.execute(
        "INSERT INTO chat_messages (thread_id, role, content) VALUES (%s, %s, %s)",
        (thread_id, role, content),
    )
    conn.commit()

def chat(conn, thread_id: str, user_text: str) -> str:
    save_message(conn, thread_id, "user", user_text)
    result = agent.invoke(
        {"messages": [{"role": "user", "content": user_text}]},
        {"configurable": {"thread_id": thread_id}},
    )
    reply = result["messages"][-1].content
    save_message(conn, thread_id, "assistant", reply)
    return reply
Enter fullscreen mode Exit fullscreen mode

The UI's history endpoint reads SELECT role, content, created_at FROM chat_messages WHERE thread_id = %s ORDER BY id. Summarization can rewrite the checkpointer as often as it likes; this table only grows.

If you serve this from FastAPI with AsyncPostgresSaver, keep the saver open for the app's lifetime (see the "connection is closed" fix).

Fix 2: keep the full transcript in state and trim per call

If you'd rather keep every message in the checkpointer, don't use a hook that rewrites state. The context engineering guide says wrap_model_call makes transient updates, "modifying what messages are sent to the model for a single call without changing what's saved in state." A sketch built on that pattern (I didn't run it):

from typing import Callable
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse

@wrap_model_call
def recent_window(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
    msgs = request.messages
    start = max(0, len(msgs) - 20)
    # start the window at a user message so a tool result never loses its tool call
    while start > 0 and msgs[start].type != "human":
        start -= 1
    return handler(request.override(messages=msgs[start:]))
Enter fullscreen mode Exit fullscreen mode

Trade-offs: state keeps growing, and the model gets no summary of older turns unless you build one yourself. Fix 1 is the simpler choice when you also want summarization.

Checklist

  1. The UI reads your table, never the thread's latest checkpointer state.
  2. Write the user message and the reply to that table on every turn; never delete from it.
  3. Set trigger on SummarizationMiddleware. Per the built-in middleware docs, "If trigger is not provided, summarization will not trigger automatically." keep defaults to ('messages', 20).
  4. Call setup() the first time you use the Postgres checkpointer.
  5. Leave LangGraph's own checkpoint tables to LangGraph.

Common questions

Do I need a separate table for the summary?
Not for the model. Per the context engineering guide, the middleware replaces older messages "with a summary message in State", so the checkpointer already holds it. Store a copy only if you want to show it in the UI.

When should summarization run?
trigger accepts fraction (of the model's context size), tokens or messages. A list of conditions fires when any is met; a dict fires only when all its thresholds are met. The docs note that fraction relies on the chat model's profile data (with langchain>=1.1); if that isn't available, use another condition.

Is the summary long-term memory?
No. Short-term memory lives "within a single thread or conversation", so a new thread starts without it. For information across threads, the docs point to long-term memory through a store.

If facts need to outlive the thread

The MemorySync LangGraph guide adds memory across threads without touching either fix above. Its context injection (MemorySyncMemoryMiddleware for create_agent, or a pre-model hook for create_react_agent) shapes only what the model sees for that call; the guide says "checkpointed graph state is never mutated". To save, a persist node (or the middleware's auto_persist flag, off by default) sends the latest user message to MemorySync, where durable facts are extracted server-side. There's also MemorySyncStore, a LangGraph BaseStore. It doesn't replace your messages table or the checkpointer. Read the guide first. For an overview of MemorySync, see the MemorySync homepage.

Skip the plumbing with MemorySync

This doesn't change the fix above; it's the memory layer around it. From the guide:

pip install "langchain-memorysync[langgraph]"
Enter fullscreen mode Exit fullscreen mode
# create_agent — the middleware pattern
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 — the pre-model hook
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"),
)
Enter fullscreen mode Exit fullscreen mode

Snippet adapted from the MemorySync LangGraph guide; I haven't run it for this page.

Sources

  • forum.langchain.com/t/langgraph-postgresql-chat-history-and-summarization-best-practice/3521 (open, 1 reply, no accepted answer; read 2026-10-28)
  • docs.langchain.com/oss/python/langchain/context-engineering (transient vs persistent context, summarization; read 2026-10-28)
  • docs.langchain.com/oss/python/langchain/short-term-memory (Postgres checkpointer, trim and summarize examples; read 2026-10-28)
  • docs.langchain.com/oss/python/langchain/middleware/built-in (SummarizationMiddleware trigger and keep; read 2026-10-28)
  • MemorySync docs: docs.memorysync.io/guides/langgraph

Next step: read the MemorySync LangGraph guide, then see MemorySync. Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).

Top comments (0)