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)
@eavanvalkenburg is already working on this.
Since Sep 19, 2026.
Assessment
This issue has not been assessed yet.
Description
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
PerServiceCallHistoryPersistingMiddlewarepersists each individual model-call's new messages incrementally, mid-turn (_sessions.py: PerServiceCallHistoryPersistingMiddleware._persist_service_call_response).- A message generated in an early loop iteration is persisted while
_excludedis not yet set. - A later loop iteration's
compaction_strategycallsset_excluded(message, excluded=True)on the same object (_compaction.py:788-794), which mutatesadditional_propertiesin place — no copy is made. - Because
InMemoryHistoryProvider.save_messages()stores by reference, the mutation is now visible in already-persistedstate["messages"], without ever going through any turn-level "is this a legitimate, summarized exclusion" check. get_messages(skip_excluded=True)silently drops this message on every future read. No summary was ever created for it (that only happens inCompactionProvider.after_run(), which — being a plainContextProvider, not aHistoryProvider— 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
ChatMiddlewareLayercopies 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_messagesdivergence 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=Truetiming race described here.
- Dominant language
- Python
- Stars
- 13.6k
- Forks
- 2.3k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 342
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from microsoft/agent-framework
-
python triage
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
microsoft/agent-framework#8523 · 1 comment ·
-
.NET compaction documentation
Difficulty 1/5 Under an hour Newbie friendliness 82/100
microsoft/agent-framework#4629 · 1 comment ·
-
harness python reproduced
microsoft/agent-framework#8567 · 2 comments · 1 assignee ·
-
.NET agents reproduced
microsoft/agent-framework#8566 · 1 comment · 1 assignee ·
-
.NET agents python
microsoft/agent-framework#8562 · 1 assignee ·
All issues in microsoft/agent-framework
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100