From a1b55fd4f9c0f4bfc31230f0c761423fd5958f85 Mon Sep 17 00:00:00 2001 From: afjcjsbx Date: Mon, 4 May 2026 14:10:42 +0200 Subject: [PATCH 1/2] fix(seahorse): enforce target token thresholds for leaf summaries --- pkg/seahorse/short_compaction.go | 6 +-- pkg/seahorse/short_compaction_test.go | 64 +++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/pkg/seahorse/short_compaction.go b/pkg/seahorse/short_compaction.go index 30e290926..0dfb1330f 100644 --- a/pkg/seahorse/short_compaction.go +++ b/pkg/seahorse/short_compaction.go @@ -602,8 +602,8 @@ func (e *CompactionEngine) generateLeafSummary( } } - // Check if level 1 succeeded - if content != "" && tokenizer.EstimateMessageTokens(providers.Message{Content: content}) < inputTokens { + // Level 1 only succeeds if it actually reaches the requested target size. + if content != "" && tokenizer.EstimateMessageTokens(providers.Message{Content: content}) <= targetTokens { return content, nil } @@ -627,7 +627,7 @@ func (e *CompactionEngine) generateLeafSummary( return "", err } } - if content != "" && tokenizer.EstimateMessageTokens(providers.Message{Content: content}) < inputTokens { + if content != "" && tokenizer.EstimateMessageTokens(providers.Message{Content: content}) <= aggressiveTarget { return content, nil } diff --git a/pkg/seahorse/short_compaction_test.go b/pkg/seahorse/short_compaction_test.go index ea7dcb52d..da07cdab7 100644 --- a/pkg/seahorse/short_compaction_test.go +++ b/pkg/seahorse/short_compaction_test.go @@ -3,6 +3,7 @@ package seahorse import ( "context" "fmt" + "strings" "sync" "sync/atomic" "testing" @@ -697,6 +698,69 @@ func TestGenerateLeafSummaryEscalationToAggressive(t *testing.T) { } } +func TestGenerateLeafSummaryEscalatesWhenLevel1MissesTarget(t *testing.T) { + var calls []string + normalContent := strings.Repeat("n", 1000) // ~404 tokens: below input, above target + aggressiveContent := strings.Repeat("a", 450) // ~184 tokens: within aggressive target + escalateComplete := func(ctx context.Context, prompt string, opts CompleteOptions) (string, error) { + if contains(prompt, "Aggressive summary policy") { + calls = append(calls, "aggressive") + return aggressiveContent, nil + } + calls = append(calls, "normal") + return normalContent, nil + } + + s := openTestStore(t) + ce, _ := newTestCompactionEngineWithStore(s, escalateComplete) + + msgs := []Message{ + {Role: "user", Content: "hello world", TokenCount: 500}, + {Role: "assistant", Content: "response", TokenCount: 500}, + } + + content, err := ce.generateLeafSummary(context.Background(), msgs, "") + if err != nil { + t.Fatalf("generateLeafSummary: %v", err) + } + if content != aggressiveContent { + t.Fatalf("expected aggressive summary after level 1 missed target") + } + if len(calls) != 2 || calls[0] != "normal" || calls[1] != "aggressive" { + t.Fatalf("expected normal then aggressive calls, got %v", calls) + } +} + +func TestGenerateLeafSummaryAcceptsContentAtTargetBoundary(t *testing.T) { + exactTargetContent := strings.Repeat("x", 488) // (488 + 12) * 2 / 5 = 200 tokens + var aggressiveCalled bool + complete := func(ctx context.Context, prompt string, opts CompleteOptions) (string, error) { + if contains(prompt, "Aggressive summary policy") { + aggressiveCalled = true + } + return exactTargetContent, nil + } + + s := openTestStore(t) + ce, _ := newTestCompactionEngineWithStore(s, complete) + + msgs := []Message{ + {Role: "user", Content: "hello world", TokenCount: 286}, + {Role: "assistant", Content: "response", TokenCount: 286}, + } + + content, err := ce.generateLeafSummary(context.Background(), msgs, "") + if err != nil { + t.Fatalf("generateLeafSummary: %v", err) + } + if content != exactTargetContent { + t.Fatalf("expected level 1 summary at target boundary to be accepted") + } + if aggressiveCalled { + t.Fatal("did not expect aggressive retry when level 1 hit target exactly") + } +} + func TestGenerateLeafSummaryEscalationToTruncation(t *testing.T) { // Both normal and aggressive return empty, should escalate to level 3 truncation emptyComplete := func(ctx context.Context, prompt string, opts CompleteOptions) (string, error) { From d601b752681028ea20fba95763d0d698d3f686d0 Mon Sep 17 00:00:00 2001 From: xp Date: Tue, 5 May 2026 19:36:09 +0800 Subject: [PATCH 2/2] fix(agent): send SVG attachments as files --- pkg/agent/agent_utils.go | 8 +++- pkg/agent/agent_utils_test.go | 76 +++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 pkg/agent/agent_utils_test.go diff --git a/pkg/agent/agent_utils.go b/pkg/agent/agent_utils.go index 2651fb2db..9228b6d55 100644 --- a/pkg/agent/agent_utils.go +++ b/pkg/agent/agent_utils.go @@ -285,6 +285,12 @@ func inferMediaType(filename, contentType string) string { ct := strings.ToLower(contentType) fn := strings.ToLower(filename) + // SVG is an image MIME type, but raster-only delivery endpoints such as + // Telegram SendPhoto reject it. Treat it as a file/document instead. + if strings.HasPrefix(ct, "image/svg") || filepath.Ext(fn) == ".svg" { + return "file" + } + if strings.HasPrefix(ct, "image/") { return "image" } @@ -298,7 +304,7 @@ func inferMediaType(filename, contentType string) string { // Fallback: infer from extension ext := filepath.Ext(fn) switch ext { - case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg": + case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp": return "image" case ".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac", ".wma", ".opus": return "audio" diff --git a/pkg/agent/agent_utils_test.go b/pkg/agent/agent_utils_test.go new file mode 100644 index 000000000..6612a60b3 --- /dev/null +++ b/pkg/agent/agent_utils_test.go @@ -0,0 +1,76 @@ +package agent + +import "testing" + +func TestInferMediaType(t *testing.T) { + tests := []struct { + name string + filename string + contentType string + want string + }{ + { + name: "png content type", + filename: "diagram", + contentType: "image/png", + want: "image", + }, + { + name: "jpeg extension fallback", + filename: "photo.JPG", + contentType: "", + want: "image", + }, + { + name: "svg content type is file", + filename: "diagram", + contentType: "image/svg+xml", + want: "file", + }, + { + name: "svg content type with parameters is file", + filename: "diagram", + contentType: "image/svg+xml; charset=utf-8", + want: "file", + }, + { + name: "svg extension fallback is file", + filename: "diagram.SVG", + contentType: "", + want: "file", + }, + { + name: "audio content type", + filename: "voice", + contentType: "audio/ogg", + want: "audio", + }, + { + name: "ogg application content type", + filename: "voice.ogg", + contentType: "application/ogg", + want: "audio", + }, + { + name: "video extension fallback", + filename: "clip.MP4", + contentType: "", + want: "video", + }, + { + name: "unknown type", + filename: "archive.bin", + contentType: "application/octet-stream", + want: "file", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := inferMediaType(tt.filename, tt.contentType) + if got != tt.want { + t.Fatalf("inferMediaType(%q, %q) = %q, want %q", tt.filename, tt.contentType, got, tt.want) + } + }) + } +}