Skip to main content

Pydantic AI SummarizingCompaction(keep_user_messages=True) drops recent messages

Messages trimmed for retained user turns skip the summary. Turn the flag off, use keep_tokens, or keep the full transcript.

Updated 6 min readMarkdown

Disclosure: I work on MemorySync (I'm Mohammed, the founder). I drafted this page with AI assistance and checked it against the source issue and the official docs listed under Sources on Oct 29, 2026. I read the issue, the pydantic-ai-harness compaction README and the Pydantic AI message history docs; I didn't run a reproducer for this page.

The question

In pydantic/pydantic-ai#11804 (opened Oct 29, 2026), a user found that SummarizingCompaction(keep_user_messages=True) in message-count mode silently loses messages. With this config on an 11-turn history:

SummarizingCompaction(
    model=..., max_messages=1, keep_messages=4,
    keep_user_messages=True, preserve_first_user_message=False,
)

the summarizer saw U1-U9, the result held the summary, retained copies of U6-U9, and U11, and this was the check output:

A9 in summary input: False | in result: False
U10 in summary input: False | in result: False
A10 in summary input: False | in result: False

So A9, U10 and A10 end up in neither the summary nor the kept history. Versions in the report: pydantic-ai 2.63.0, pydantic-ai-harness 0.63.0 (the latest harness release on PyPI as of Nov 7, 2026 is 0.66.0, and its compact() still has this path).

Status (checked Nov 7, 2026): open, labelled needs-maintainer-discussion, no fix merged. One commenter reproduced it and traced it to compact(). A maintainer opened PR #12652 on Nov 6, 2026 to summarize the displaced tail messages instead of dropping them; it isn't merged yet, and the latest harness release (0.66.0) still has the old code path. An earlier fix, #9715, only covered the case where the current request itself was dropped (#9713).

Skip the memory plumbing around this fix. Once the fix below is in, the MemorySync Pydantic AI 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

From the issue's walk-through of _summarizing_compaction.py:

  1. compact() picks cutoff from keep_messages, sets to_summarize = split[:cutoff] and preserved = messages[cutoff:].
  2. With keep_user_messages=True, it copies newer user turns out of the summarized prefix, then trims preserved from the front to make room for them.
  3. The summarizer still gets the original to_summarize.

The messages trimmed in step 2 come after cutoff, so they never reach the summarizer, and they're gone from the result too. Each retained user copy costs one recent message.

The compaction README does say retained user turns "consume the existing keep_messages tail budget" in message-count mode. What it doesn't say is that the trimmed tail skips the summary.

Fix 1: turn off keep_user_messages for now

The reporter notes that with keep_user_messages=False the same messages "survive in the tail". If full coverage matters more to you than verbatim user turns, drop the flag until a fix lands:

SummarizingCompaction(max_tokens=120_000, keep_messages=20)

Fix 2: use token mode

The README describes a different rule when keep_tokens is positive: "the original suffix takes precedence: keep_messages caps only the additional older user turns, which do not reduce the retained tail." The bug report and the code path it points to are for message-count mode (keep_tokens is None). If you want to keep keep_user_messages=True, a positive keep_tokens avoids the shrinking tail per the README. Test it on your own history, since the issue doesn't include a token-mode repro.

Fix 3: keep the full transcript outside the compacted history

Compaction changes what the model sees. It doesn't have to be your only copy. The Pydantic AI message history docs show saving result.all_messages() to JSON and loading it back with ModelMessagesTypeAdapter:

from pydantic_ai import ModelMessagesTypeAdapter
from pydantic_core import to_jsonable_python

history = result.all_messages()
as_json = to_jsonable_python(history)   # store this in your own DB
restored = ModelMessagesTypeAdapter.validate_python(as_json)

If you write that after each run, messages the compaction step drops are still in your store, and you can re-run compaction once a fixed release is out.

Checklist

  1. Using SummarizingCompaction(keep_user_messages=True) with keep_messages and no keep_tokens? You're on the affected path.
  2. Short term: set keep_user_messages=False, or move to a positive keep_tokens.
  3. Save result.all_messages() somewhere you control so nothing is lost for good.
  4. Watch #11804 and PR #12652, and check the harness changelog before turning the flag back on.

Common questions

Is the current user request lost? No. That case was #9713, fixed by #9715, which reserves one tail slot for the request being answered.

How many messages go missing? Per the issue, one recent message per retained user copy in message-count mode. With keep_messages=20 on the same history, A1 was lost the same way.

Does a bigger keep_messages fix it? Not on its own. The issue shows the loss still happens; it just moves.

Skip the plumbing with MemorySync

Disclosure: I build MemorySync.

It doesn't fix this bug, and it isn't a transcript backup. The MemorySync Pydantic AI guide documents MemorySyncCapability: after each successful run it sends the user's prompt to fact extraction and stores the durable facts (not the prompt text, and not model replies), and it adds recall through the instructions channel rather than as message parts. So user facts from a turn that compaction later drops can still come back as that user's memories in later runs. For the full message list, use Fix 3. Read the guide first. For an overview of MemorySync, see the MemorySync homepage.

From the guide:

Unverified (not run):

pip install pydantic-ai-memorysync
from pydantic_ai import Agent
from pydantic_ai_memorysync import MemorySyncCapability

agent = Agent(
    "openai:your-model",
    instructions="You are a helpful assistant.",
    capabilities=[MemorySyncCapability(user_id="customer-7")],
)

result = await agent.run("What should I cook tonight?")
# The model saw:
#   You are a helpful assistant.
#
#   Relevant memories about this user from previous conversations:
#   - Is vegetarian
#   - Flies aisle
#
# ...and the durable facts in this prompt are remembered for
# every future run.

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

Try it: MemorySync is optional here: the steps above work without it. If you want the memory layer from the snippet, see MemorySync, create an API key in the dashboard and drop the snippet into your agent.

Sources

  • github.com/pydantic/pydantic-ai/issues/11804 (opened 2026-10-29, open, 2 comments; read 2026-11-07)
  • github.com/pydantic/pydantic-ai/pull/12652 (open, unmerged; read 2026-11-07)
  • pydantic-ai-harness compaction README, "Keeping user messages (keep_user_messages)" (main branch; read 2026-10-29)
  • ai.pydantic.dev/message-history/ (all_messages(), ModelMessagesTypeAdapter; read 2026-10-29)
  • MemorySync docs: docs.memorysync.io/guides/pydantic-ai

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

About the author

  • Building MemorySync (memorysync.io) — persistent memory infrastructure for AI agents, Cursor, and Claude Code.

No comments yet. Be the first to comment.

More from this blog