Python: [Bug]: require_per_service_call_history_persistence=True combined with a per-call compaction_strategy can persist excluded messages with no summary (silent history loss)

Open
#8,546 4 comments 0 reactions 1 assignee View on GitHub

@eavanvalkenburg is already working on this.

Since Sep 19, 2026.

Assessment

This issue has not been assessed yet.

Description

agents compaction middleware python reproduced
Description

Summary
When Agent(..., require_per_service_call_history_persistence=True) is combined with a per-call compaction_strategy (before_compaction_strategy) that calls set_excluded() on messages, a message can be persisted to the HistoryProvider before it is later excluded by set_excluded() in a subsequent tool-call loop iteration. Because set_excluded() mutates the Message object's additional_properties in place, and InMemoryHistoryProvider stores messages by reference (no deep copy), the later mutation leaks into already-persisted state with no corresponding summary message (only CompactionProvider.after_run(), which runs once per agent.run(), creates paired summaries). On the next get_messages(skip_excluded=True) call, that message is filtered out permanently — content is lost with no trace.

Mechanism

  1. PerServiceCallHistoryPersistingMiddleware persists each individual model-call's new messages incrementally, mid-turn (_sessions.py: PerServiceCallHistoryPersistingMiddleware._persist_service_call_response).
  2. A message generated in an early loop iteration is persisted while _excluded is not yet set.
  3. A later loop iteration's compaction_strategy calls set_excluded(message, excluded=True) on the same object (_compaction.py:788-794), which mutates additional_properties in place — no copy is made.
  4. Because InMemoryHistoryProvider.save_messages() stores by reference, the mutation is now visible in already-persisted state["messages"], without ever going through any turn-level "is this a legitimate, summarized exclusion" check.
  5. get_messages(skip_excluded=True) silently drops this message on every future read. No summary was ever created for it (that only happens in CompactionProvider.after_run(), which — being a plain ContextProvider, not a HistoryProvider — is unaffected by the flag and still only runs once per turn, and by design never considers the current turn's own new messages as compaction candidates).

Without require_per_service_call_history_persistence, this doesn't happen: history is persisted once, after the whole tool-call loop completes, so all in-turn set_excluded() calls have already landed on the final message objects before persistence — a caller-side HistoryProvider can then strip stray exclusion flags on any message it's about to persist for the first time (which is what we do downstream). Enabling per-service-call persistence defeats that pattern, since a message can be persisted before it's excluded, and no hook re-runs when it's mutated afterward.

Expected behavior
PerServiceCallHistoryPersistingMiddleware persists snapshots/copies of messages rather than live references, so later mutations don't leak into already-persisted state

Code Sample

Error Messages / Stack Traces

Package Versions

agent-framework 1.17.0

Python Version

python 3.14

Additional Context

Related issues
Similar symptom (exclusion flag persists, summary does not, skip_excluded=True silently drops content forever) has been reported before, but via different root causes — this appears to be a distinct case:

  • #7744 (fixed by #7912, in 1.17.0): summary lost mid-run because ChatMiddlewareLayer copies the message list; already fixed and does not reproduce here.
  • #8099 (closed 2026-09-17): same "flags persist, summary doesn't" symptom, but caused by function_call_messages/prepared_messages divergence in the default once-per-run persistence path (_tools.py, non-streaming). That fix does not address the per-service-call/require_per_service_call_history_persistence=True timing race described here.
Dominant language
Python
Stars
13.6k
Forks
2.3k
Avg merge
1d 20h
Merged PRs (30d)
342

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from microsoft/agent-framework

All issues in microsoft/agent-framework

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.