Skip to content

Keep retained history Source when compaction runs across turns - #934

Merged
Quim Muntal (qmuntal) merged 3 commits into
microsoft:mainfrom
PratikDhanaveFork:fix-compaction-source-mislabel-across-turns
Aug 31, 2026
Merged

Keep retained history Source when compaction runs across turns#934
Quim Muntal (qmuntal) merged 3 commits into
microsoft:mainfrom
PratikDhanaveFork:fix-compaction-source-mislabel-across-turns

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

Problem

markGeneratedMessages (in agent/compaction/provider.go) tells provider-generated messages apart from the caller's original input by *message.Message pointer identity. That invariant only holds on the first, session-less turn.

With a session, on every subsequent turn the index is rebuilt from persisted state.MessageGroups (NewMessageIndex(...) + index.Update(messages)) — or from a deserialized session — whose retained-history messages are different pointers from this turn's incoming messages. Each retained history message therefore fails the identity check and is stamped Source = {SourceTypeContextProvider, sourceID}.

Source.Type is load-bearing: the default history provider's store filter (notSourceTypes(SourceTypeHistoryProvider)) and context-provider attribution key on it, so this relabels genuine user/assistant history as provider-generated on every multi-turn run (and after any session serialize/deserialize round-trip).

Fix

Identify provider-generated messages by comparing against this turn's input by content (messageContentEqual) rather than by pointer identity. Retained prior-turn history content-matches the incoming messages and keeps its original Source; only the summary messages compaction actually generates (which never match an input message) get stamped.

Test

TestNewProvider_KeepsRetainedHistorySourceAcrossTurns drives two turns against one session and asserts the retained first-turn messages keep their original Source. Fails before the fix (prior-turn messages stamped context-provider), passes after. Existing source-stamping tests stay green.

markGeneratedMessages told provider-generated messages apart from original
input by *message.Message pointer identity. That only holds on the first,
session-less turn. With a session the index is rebuilt from persisted
state.MessageGroups (or a deserialized session), whose message pointers
differ from this turn's input, so every retained history message failed the
identity check and was stamped Source={context-provider, sourceID}.

Source.Type is load-bearing - the default history provider's store filter
and context-provider attribution key on it - so this relabeled genuine
user/assistant history as provider-generated on every multi-turn run.

Match this turn's input by content instead of pointer identity, so retained
prior-turn history keeps its original Source and only the summary messages
compaction actually generates are stamped.
Copilot AI lite review requested due to automatic review settings August 28, 2026 05:39
@PratikDhanave
PratikDhanave (PratikDhanave) requested a review from a team as a code owner August 28, 2026 05:39
@github-actions github-actions Bot added area:agent Changes files in the agent area size:medium At most 100 changed lines across at most 5 files pending-auto-risk Automatic risk classification is in progress parity-approved Go API consistency review found no parity issues labels Aug 28, 2026
@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes incorrect Source attribution for retained history messages when the compaction provider runs across multiple turns with a session (where message pointers are reconstructed from persisted state and no longer match the current turn’s input pointers).

Changes:

  • Update markGeneratedMessages to identify “provider-generated” messages by content equality rather than pointer identity.
  • Add a helper (containsMessageByContent) to support content-based membership checks.
  • Add a regression test ensuring retained prior-turn history keeps its original Source across turns in a single session.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
agent/compaction/provider.go Switch source-stamping logic from pointer-identity to content-based matching to avoid relabeling retained history across turns.
agent/compaction/compaction_test.go Add a multi-turn session test asserting retained history messages are not mislabeled as context-provider generated.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread agent/compaction/provider.go
@github-actions github-actions Bot added risk:medium Contained production impact requiring normal review depth and removed pending-auto-risk Automatic risk classification is in progress labels Aug 28, 2026

@qmuntal Quim Muntal (qmuntal) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not fully at parity with .NET yet. The content-equality check matches .NET, but .NET also marks messages restored from compaction state as message history before calling Update.

Without that step, retained Go messages keep their stale source, often the zero source from the first turn. The default history filter then stores them again. I reproduced this on the PR head: after two turns, stored history was [u1 a1 u1 u2 a2].

Could we mirror the .NET pre-update history attribution and extend the test through the history-provider lifecycle? It should verify that retained messages are history-attributed, new messages are not, and stored history contains no duplicates.

Match .NET's CompactionProvider: when the message index is rebuilt from
persisted state, stamp every restored message with the history source before
folding in this turn's input, and skip already-history messages in
markGeneratedMessages. Together with the content-equality check this keeps
genuine prior-turn history attributed as chat history (not context-provider
generated) and stops it being re-stored as new messages at the end of a run.
@PratikDhanave

Copy link
Copy Markdown
Contributor Author

Addressed — now at parity with the .NET CompactionProvider:

  • When the index is rebuilt from persisted state, every restored message is stamped with the history source (SourceTypeHistoryProvider) before index.Update(messages), mirroring .NET's "treat all messages already in the index as chat history" step.
  • markGeneratedMessages now also skips messages already marked as history (in addition to the content-equality check), so restored history keeps its chat-history attribution and only the summaries the strategies generate are stamped as context-provider.

Net effect: genuine prior-turn history is attributed as chat history (not context-provider generated) and isn't re-stored at the end of the run. The regression test now asserts the retained messages carry the history source.

@github-actions github-actions Bot added pending-auto-risk Automatic risk classification is in progress and removed risk:medium Contained production impact requiring normal review depth labels Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Go API Consistency Review

Scope: user-visible behavior (bug fix), internal-only (no exported API change)

Changed Go contract: None — no exported identifiers added, removed, or changed. The fix modifies the unexported markGeneratedMessages function in agent/compaction/provider.go to use content-based equality (messageContentEqual) instead of pointer identity when determining whether a message was generated by the compaction provider. The Source field on *message.Message is observable by callers, but this fix restores the correct value rather than changing semantics.

Upstream evidence reviewed: No equivalent compaction provider was found in microsoft/agent-framework Python (python/packages/core/agent_framework/) or .NET (dotnet/src/Microsoft.Agents.AI/). The Source/SourceType tagging and markGeneratedMessages logic are Go-specific internal plumbing with no direct upstream analogue to compare against.

Result: No parity issues. This is a self-contained bug fix to Go-internal implementation details. The corrected behavior (retained history messages keep their original Source across multi-turn sessions) aligns with the intended semantics already expressed by the surrounding Go code (notSourceTypes(SourceTypeHistoryProvider) filter, context-provider attribution). No exported API surface changed; the public-api-change label is not warranted.

Generated by Go API Consistency Review Agent · sonnet46 · 17.1 AIC · ⌖ 5 AIC · ⊞ 6.4K ·

@github-actions github-actions Bot added risk:medium Contained production impact requiring normal review depth and removed pending-auto-risk Automatic risk classification is in progress labels Aug 31, 2026
@qmuntal
Quim Muntal (qmuntal) added this pull request to the merge queue Aug 31, 2026
Merged via the queue into microsoft:main with commit 38385d4 Aug 31, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agent Changes files in the agent area parity-approved Go API consistency review found no parity issues risk:medium Contained production impact requiring normal review depth size:medium At most 100 changed lines across at most 5 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants