diff --git a/agent/compaction/index.go b/agent/compaction/index.go index 2ef46162..b14169b1 100644 --- a/agent/compaction/index.go +++ b/agent/compaction/index.go @@ -78,16 +78,23 @@ func (index *MessageIndex) Update(messages []*message.Message) { return } processedMessageCount := index.includedRawMessageCount() - if index.lastProcessedMessage != nil && len(messages) >= processedMessageCount && messageContentEqual(messages[len(messages)-1], index.lastProcessedMessage) { - return - } - + // Locate where the already-processed prefix ends. lastProcessedMessage sits + // at index processedMessageCount-1 when nothing earlier was trimmed, so check + // that expected position first. Matching only the last occurrence of its + // content would latch the boundary onto a later duplicate when a message is + // repeated (e.g. a user re-sending "continue"), silently dropping the turns + // appended since. Fall back to a search only when the prefix was trimmed. foundIndex := -1 if index.lastProcessedMessage != nil { - for i := len(messages) - 1; i >= 0; i-- { - if messageContentEqual(messages[i], index.lastProcessedMessage) { - foundIndex = i - break + if expected := processedMessageCount - 1; expected >= 0 && expected < len(messages) && + messageContentEqual(messages[expected], index.lastProcessedMessage) { + foundIndex = expected + } else { + for i := len(messages) - 1; i >= 0; i-- { + if messageContentEqual(messages[i], index.lastProcessedMessage) { + foundIndex = i + break + } } } } diff --git a/agent/compaction/index_test.go b/agent/compaction/index_test.go index 02345f7e..bb728cb0 100644 --- a/agent/compaction/index_test.go +++ b/agent/compaction/index_test.go @@ -343,3 +343,30 @@ func splitWords(text string) func(func(string) bool) { } } } + +// A user re-sending an identical short message (or any repeated content) must +// not cause Update to drop the turns appended since the previous update. The +// processed-prefix boundary must be located positionally, not by matching the +// last occurrence of the previous message's content. +func TestMessageIndex_Update_RepeatedContentDoesNotDropMessages(t *testing.T) { + turn1 := []*message.Message{ + textMessage(message.RoleUser, "hi"), + textMessage(message.RoleAssistant, "hello"), + textMessage(message.RoleUser, "continue"), + } + index := &compaction.MessageIndex{} + index.Update(turn1) + if got, want := messageTexts(index.AllMessages()), []string{"hi", "hello", "continue"}; !slices.Equal(got, want) { + t.Fatalf("after turn 1: got %v, want %v", got, want) + } + + // Turn 2: the assistant replies, then the user repeats "continue" verbatim. + turn2 := append(slices.Clone(turn1), + textMessage(message.RoleAssistant, "sure"), + textMessage(message.RoleUser, "continue"), + ) + index.Update(turn2) + if got, want := messageTexts(index.AllMessages()), []string{"hi", "hello", "continue", "sure", "continue"}; !slices.Equal(got, want) { + t.Fatalf("after turn 2 (repeated \"continue\"): got %v, want %v — appended turns were dropped", got, want) + } +}