normalize summary cache keys

This commit is contained in:
Ollie Agent 2026-08-15 13:42:11 +02:00
parent adf7242774
commit 60c4541fa4
3 changed files with 96 additions and 7 deletions

View File

@ -101,12 +101,33 @@ func toolMessageID(m backend.Message) string {
return "tool:content:" + hex.EncodeToString(h[:])
}
const coldSummaryCacheVersion = "v2"
func toolSummaryHash(m backend.Message) string {
h := sha256.New()
h.Write([]byte(m.ToolCallID))
h.Write([]byte{0})
h.Write([]byte(m.Content))
return hex.EncodeToString(h.Sum(nil))
h := sha256.Sum256([]byte(coldSummaryCacheVersion + "\x00" + normalizeSummaryCacheContent(m.Content)))
return hex.EncodeToString(h[:])
}
func normalizeSummaryCacheContent(content string) string {
if looksStructured(content) {
return content
}
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
normalized := make([]string, 0, len(lines))
blank := false
for _, line := range lines {
line = strings.TrimRight(line, " \t")
if strings.TrimSpace(line) == "" {
if blank {
continue
}
blank = true
} else {
blank = false
}
normalized = append(normalized, line)
}
return strings.TrimSpace(strings.Join(normalized, "\n"))
}
func (s *History) history() []backend.Message {

View File

@ -77,6 +77,27 @@ func TestStripColdReusesSummaryCache(t *testing.T) {
t.Fatalf("cached summary was not applied: %#v", h.messages[0])
}
}
func TestStripColdReusesSummaryCacheAcrossToolCalls(t *testing.T) {
b := &summaryBackend{}
h := newHistory("test")
for i := 0; i < 9; i++ {
m := backend.Message{Role: "user", Content: "hot"}
if i == 0 {
m = backend.Message{Role: "tool", ToolCallID: "tc-0", Content: strings.Repeat("same result ", 30)}
}
h.messages = append(h.messages, m)
}
if _, _ = h.stripCold(context.Background(), b); b.calls != 1 {
t.Fatalf("first stripCold calls = %d, want 1", b.calls)
}
h.messages[0] = backend.Message{Role: "tool", ToolCallID: "tc-0", Content: strings.Repeat("same result ", 30)}
if _, _ = h.stripCold(context.Background(), b); b.calls != 1 {
t.Fatalf("different-call cache calls = %d, want 1", b.calls)
}
if !h.messages[0].SummaryApplied || h.messages[0].Content != "summary" {
t.Fatalf("cached summary was not applied: %#v", h.messages[0])
}
}
func TestStripColdSkipsAlreadySummarizedAfterRestore(t *testing.T) {
b := &summaryBackend{}
h := RestoreHistoryFromMessages([]backend.Message{
@ -99,11 +120,17 @@ type summaryBackend struct{ calls int }
func (b *summaryBackend) ChatStream(_ context.Context, messages []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
b.calls++
if len(messages) != 1 || !strings.Contains(messages[0].Content, "id=tool:tc-0:") {
if len(messages) != 1 || !strings.Contains(messages[0].Content, "id=tool:") {
return nil, fmt.Errorf("summary request did not use stable message ID")
}
start := strings.Index(messages[0].Content, "id=") + len("id=")
end := strings.Index(messages[0].Content[start:], ":\n")
if end < 0 {
return nil, fmt.Errorf("summary request has malformed message ID")
}
id := messages[0].Content[start : start+end]
out := make(chan backend.StreamEvent, 1)
out <- backend.StreamEvent{Content: `{"tool:tc-0":"summary"}`, Done: true}
out <- backend.StreamEvent{Content: fmt.Sprintf(`{"%s":"summary"}`, id), Done: true}
close(out)
return out, nil
}

View File

@ -0,0 +1,41 @@
package agent
import (
"testing"
"ollie/cmd/olliesrv/internal/backend"
)
func messageWithContent(callID, content string) backend.Message {
return backend.Message{Role: "tool", ToolCallID: callID, Content: content}
}
func TestNormalizeSummaryCacheContentPlainText(t *testing.T) {
got := normalizeSummaryCacheContent(" first line \r\n\r\n\nsecond line \t\n\n")
if got != "first line\n\nsecond line" {
t.Fatalf("normalized content = %q", got)
}
}
func TestNormalizeSummaryCacheContentPreservesStructuredContent(t *testing.T) {
content := "{\r\n \"key\": \"value\"\r\n}\r\n"
if got := normalizeSummaryCacheContent(content); got != content {
t.Fatalf("structured content changed: %q", got)
}
}
func TestToolSummaryHashIgnoresToolCallID(t *testing.T) {
first := toolSummaryHash(messageWithContent("first-call", "same result\n"))
second := toolSummaryHash(messageWithContent("second-call", "same result"))
if first != second {
t.Fatalf("hashes differ for identical content: %q != %q", first, second)
}
}
func TestToolSummaryHashSeparatesContent(t *testing.T) {
first := toolSummaryHash(messageWithContent("call", "first result"))
second := toolSummaryHash(messageWithContent("call", "second result"))
if first == second {
t.Fatal("different content produced the same hash")
}
}