diff --git a/go.mod b/go.mod index 563a493fb..c42b04447 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/bobbyjohnstx/tinycode -go 1.27.1 +go 1.25.0 require ( github.com/charmbracelet/bubbles v1.0.0 diff --git a/internal/server/session_ops_test.go b/internal/server/session_ops_test.go index 1bbba1b07..d8ccfe900 100644 --- a/internal/server/session_ops_test.go +++ b/internal/server/session_ops_test.go @@ -354,7 +354,7 @@ func TestPersistPromptResult_CompactedReplacesHistory(t *testing.T) { } compacted := []session.Message{ - {ID: "summary", SessionID: info.ID, Role: session.RoleSystem, Parts: []session.Part{session.TextPart("summary")}, CreatedAt: now}, + {ID: "summary", SessionID: info.ID, Role: session.RoleUser, Parts: []session.Part{session.TextPart("summary")}, CreatedAt: now}, {ID: "keep", SessionID: info.ID, Role: session.RoleUser, Parts: []session.Part{session.TextPart("keep")}, CreatedAt: now.Add(time.Second)}, {ID: "reply", SessionID: info.ID, Role: session.RoleAssistant, Parts: []session.Part{session.TextPart("reply")}, CreatedAt: now.Add(2 * time.Second)}, } diff --git a/internal/session/processor_llm.go b/internal/session/processor_llm.go index a2768dd25..8b03d2f7c 100644 --- a/internal/session/processor_llm.go +++ b/internal/session/processor_llm.go @@ -337,10 +337,13 @@ func (p *Processor) compact(ctx context.Context) (bool, error) { summary := strings.Join(summaryParts, "") summaryMsgID, _ := id.Ascending("message") + // Use a user-role message: some providers reject conversations whose + // first non-system message is a system message ("no user query found in + // messages", HTTP 500). summaryMsg := Message{ ID: summaryMsgID, SessionID: p.config.SessionID, - Role: RoleSystem, + Role: RoleUser, Parts: []Part{TextPart(summary)}, CreatedAt: time.Now(), } diff --git a/internal/session/processor_test.go b/internal/session/processor_test.go index 7542d3dc6..bc1223956 100644 --- a/internal/session/processor_test.go +++ b/internal/session/processor_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "testing" "time" @@ -650,6 +651,58 @@ func TestProcessor_BelowThreshold_NoCompaction(t *testing.T) { } } +// Issue #23: after compaction the conversation must start with a user-role +// message, otherwise providers reject it with HTTP 500 "no user query found +// in messages". +func TestProcessor_Compact_SummaryIsUserRole(t *testing.T) { + // Build a conversation (18 x ~2000 est. tokens = ~36k) that exceeds the + // 15k preserve budget so compaction actually runs and preserves a tail. + seed := make([]Message, 0, 18) + for i := 0; i < 9; i++ { + seed = append(seed, + Message{ID: fmt.Sprintf("u%d", i), SessionID: "ses_c23", Role: RoleUser, Parts: []Part{TextPart("user turn " + strings.Repeat("x", 8000)+fmt.Sprintf("%d", i))}}, + Message{ID: fmt.Sprintf("a%d", i), SessionID: "ses_c23", Role: RoleAssistant, Parts: []Part{TextPart("assistant turn " + strings.Repeat("y", 8000)+fmt.Sprintf("%d", i))}}, + ) + } + p := NewProcessor(ProcessorConfig{ + SessionID: "ses_c23", + Model: &provider.Model{ID: "test-model"}, + Compaction: DefaultCompactionConfig(), + AutoContinueMax: -1, + }, &mockLLMClient{ + responses: []mockResponse{ + {events: []llm.Event{ // compaction summary call + {Type: llm.EventTextDelta, Text: "summary of earlier work"}, + {Type: llm.EventFinish, FinishReason: "stop", Usage: &llm.Usage{PromptTokens: 100, CompletionTokens: 10}}, + }}, + }, + }, &stubToolExecutor{}, bus.New()) + p.SetMessages(seed) + + compacted, err := p.Compact(context.Background()) + if err != nil { + t.Fatalf("Compact: %v", err) + } + if !compacted { + t.Fatal("expected compaction to run, got no-op") + } + + msgs := p.Messages() + if msgs[0].Role != RoleUser { + t.Fatalf("first message after compaction has role %q, want %q", msgs[0].Role, RoleUser) + } + hasUser := false + for _, m := range msgs { + if m.Role == RoleUser { + hasUser = true + break + } + } + if !hasUser { + t.Fatal("compacted conversation has no user-role message at all") + } +} + // --- Issue #89: statusFromError anchored HTTP matching --- func TestStatusFromError_PositiveMatches(t *testing.T) { diff --git a/internal/session/session_test.go b/internal/session/session_test.go index 40f68d991..180d82dec 100644 --- a/internal/session/session_test.go +++ b/internal/session/session_test.go @@ -703,7 +703,7 @@ func TestMessageStore_ReplaceAll(t *testing.T) { } compacted := []Message{ - {ID: "summary", SessionID: ses.ID, Role: RoleSystem, Parts: []Part{TextPart("summary")}, CreatedAt: now}, + {ID: "summary", SessionID: ses.ID, Role: RoleUser, Parts: []Part{TextPart("summary")}, CreatedAt: now}, {ID: "keep", SessionID: ses.ID, Role: RoleUser, Parts: []Part{TextPart("keep")}, CreatedAt: now.Add(time.Second)}, {ID: "new", SessionID: ses.ID, Role: RoleAssistant, Parts: []Part{TextPart("new")}, CreatedAt: now.Add(2 * time.Second)}, }