diff --git a/agent/compaction/compaction_test.go b/agent/compaction/compaction_test.go index d1525ae0..bb7abf59 100644 --- a/agent/compaction/compaction_test.go +++ b/agent/compaction/compaction_test.go @@ -696,3 +696,40 @@ func messageTexts(messages []*message.Message) []string { } return texts } + +func TestNewProvider_KeepsRetainedHistorySourceAcrossTurns(t *testing.T) { + // A compaction provider generates only summary messages; genuine prior-turn + // history returned from the persisted index must keep its original Source. + // Across turns the index is rebuilt from persisted groups whose message + // pointers differ from this turn's input, so identity-based attribution + // wrongly stamps real history as context-provider generated. + provider := compaction.NewContextProvider(compaction.ContextProviderConfig{ + Strategy: &compaction.TruncationStrategy{Trigger: compaction.Never()}, + SourceID: "compaction-test", + }) + session := agenttest.CreateSession() + + if _, _, err := invokeProvider(provider, t.Context(), []*message.Message{ + textMessage(message.RoleUser, "u1"), + textMessage(message.RoleAssistant, "a1"), + }, agent.WithSession(session)); err != nil { + t.Fatalf("turn 1: %v", err) + } + + out, _, err := invokeProvider(provider, t.Context(), []*message.Message{ + textMessage(message.RoleUser, "u1"), + textMessage(message.RoleAssistant, "a1"), + textMessage(message.RoleUser, "u2"), + textMessage(message.RoleAssistant, "a2"), + }, agent.WithSession(session)) + if err != nil { + t.Fatalf("turn 2: %v", err) + } + + cp := message.Source{Type: agent.SourceTypeContextProvider, ID: "compaction-test"} + for i, msg := range out { + if msg.Source == cp { + t.Errorf("message %d (%q) mislabeled as context-provider generated; genuine history must keep its original Source", i, msg.String()) + } + } +} diff --git a/agent/compaction/provider.go b/agent/compaction/provider.go index 4e03c1f8..51825443 100644 --- a/agent/compaction/provider.go +++ b/agent/compaction/provider.go @@ -128,16 +128,17 @@ func (p *contextProvider) markGeneratedMessages(messages, inputMessages []*messa if len(messages) == 0 { return messages } - originals := make(map[*message.Message]struct{}, len(inputMessages)) - for _, msg := range inputMessages { - originals[msg] = struct{}{} - } source := message.Source{Type: agent.SourceTypeContextProvider, ID: p.sourceID} for i, msg := range messages { - if _, ok := originals[msg]; ok { + if msg == nil || msg.Source == source { continue } - if msg == nil || msg.Source == source { + // A message is provider-generated only when it is not one of this turn's + // input messages. Compare by content, not pointer identity: with a session + // the index is rebuilt from persisted groups whose message pointers differ + // from the incoming messages, so an identity check would wrongly stamp + // genuine prior-turn history as context-provider generated. + if containsMessageByContent(inputMessages, msg) { continue } marked := msg.Clone() @@ -146,3 +147,12 @@ func (p *contextProvider) markGeneratedMessages(messages, inputMessages []*messa } return messages } + +func containsMessageByContent(messages []*message.Message, target *message.Message) bool { + for _, candidate := range messages { + if messageContentEqual(candidate, target) { + return true + } + } + return false +}