Skip to content

Commit 1d6f5dd

Browse files
authored
refactor: Change compact to start new conversation with summary (#356)
Changes the /compact command behavior to start a new conversation with a summary instead of compacting in-place. Simplifies the ConversationOptimizerService interface by removing OptimizeMessagesWithModel and consolidating into OptimizeMessages. Adds GetCurrentConversationTitle method to preserve conversation context. ## Key changes in this PR: - `/compact` now creates a new conversation titled "Continued from [original title]" with optimized messages - Removed redundant `OptimizeMessagesWithModel` method from interface - Added `GetCurrentConversationTitle()` to conversation repository - Updated all callers to use the simplified `OptimizeMessages` method - Improved error handling when no model is selected ## Files changed: - `internal/domain/interfaces.go` - Simplified interface, added GetCurrentConversationTitle method - `internal/handlers/chat_shortcut_handler.go` - Updated to use new compact behavior - `internal/services/agent.go` - Updated to use simplified OptimizeMessages method - `internal/services/conversation.go` - Updated to use simplified OptimizeMessages method - `internal/services/conversation_optimizer.go` - Implemented new compact behavior - `internal/services/conversation_optimizer_test.go` - Updated tests for new behavior - `internal/services/persistent_conversation.go` - Updated to use simplified OptimizeMessages method - `internal/shortcuts/core.go` - Updated to use simplified OptimizeMessages method - `tests/mocks/domain/fake_conversation_repository.go` - Updated mock to include new method
1 parent 08a143a commit 1d6f5dd

19 files changed

+177
-231
lines changed

internal/domain/interfaces.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,12 @@ type ConversationRepository interface {
103103
RemovePendingToolCallByID(toolCallID string)
104104
StartNewConversation(title string) error
105105
DeleteMessagesAfterIndex(index int) error
106+
GetCurrentConversationTitle() string
106107
}
107108

108109
// ConversationOptimizerService optimizes conversation history to reduce token usage
109110
type ConversationOptimizerService interface {
110-
OptimizeMessages(messages []sdk.Message, force bool) []sdk.Message
111-
OptimizeMessagesWithModel(messages []sdk.Message, currentModel string, force bool) []sdk.Message
111+
OptimizeMessages(messages []sdk.Message, model string, force bool) []sdk.Message
112112
}
113113

114114
// ModelService handles model selection and information

internal/handlers/chat_shortcut_handler.go

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,8 @@ func (s *ChatShortcutHandler) performCompactAsync() tea.Cmd {
507507

508508
logger.Info("Starting conversation compaction", "message_count", len(entries))
509509

510+
originalTitle := s.handler.conversationRepo.GetCurrentConversationTitle()
511+
510512
messages := make([]sdk.Message, 0, len(entries))
511513
for _, entry := range entries {
512514
if entry.Hidden {
@@ -517,14 +519,19 @@ func (s *ChatShortcutHandler) performCompactAsync() tea.Cmd {
517519

518520
currentModel := s.handler.modelService.GetCurrentModel()
519521
if currentModel == "" {
520-
logger.Warn("No current model set for compaction - will use basic summary")
522+
return domain.SetStatusEvent{
523+
Message: "No model selected - please select a model first",
524+
Spinner: false,
525+
StatusType: domain.StatusError,
526+
}
521527
}
522-
logger.Info("About to optimize conversation", "model", currentModel, "message_count", len(messages))
528+
529+
logger.Info("Optimizing conversation", "model", currentModel, "message_count", len(messages))
523530

524531
optimizedChan := make(chan []sdk.Message, 1)
525532
go func() {
526-
result := s.handler.conversationOptimizer.OptimizeMessagesWithModel(messages, currentModel, true)
527-
optimizedChan <- result
533+
optimized := s.handler.conversationOptimizer.OptimizeMessages(messages, currentModel, true)
534+
optimizedChan <- optimized
528535
}()
529536

530537
var optimizedMessages []sdk.Message
@@ -534,7 +541,7 @@ func (s *ChatShortcutHandler) performCompactAsync() tea.Cmd {
534541
case <-time.After(70 * time.Second):
535542
logger.Error("Optimization timed out after 70 seconds")
536543
return domain.SetStatusEvent{
537-
Message: "Conversation compaction timed out - try again or check gateway logs",
544+
Message: "Conversation optimization timed out - try again or check gateway logs",
538545
Spinner: false,
539546
StatusType: domain.StatusError,
540547
}
@@ -548,10 +555,11 @@ func (s *ChatShortcutHandler) performCompactAsync() tea.Cmd {
548555
}
549556
}
550557

551-
if clearErr := s.handler.conversationRepo.Clear(); clearErr != nil {
552-
logger.Error("failed to clear conversation during compaction", "error", clearErr)
558+
newTitle := fmt.Sprintf("Continued from %s", originalTitle)
559+
if err := s.handler.conversationRepo.StartNewConversation(newTitle); err != nil {
560+
logger.Error("Failed to start new conversation", "error", err)
553561
return domain.SetStatusEvent{
554-
Message: fmt.Sprintf("Failed to compact conversation: %v", clearErr),
562+
Message: fmt.Sprintf("Failed to start new conversation: %v", err),
555563
Spinner: false,
556564
StatusType: domain.StatusError,
557565
}
@@ -560,29 +568,14 @@ func (s *ChatShortcutHandler) performCompactAsync() tea.Cmd {
560568
for _, msg := range optimizedMessages {
561569
entry := domain.ConversationEntry{
562570
Message: msg,
571+
Model: currentModel,
563572
Time: time.Now(),
564573
}
565-
if addErr := s.handler.conversationRepo.AddMessage(entry); addErr != nil {
566-
logger.Error("failed to add optimized message during compaction", "error", addErr)
574+
if err := s.handler.conversationRepo.AddMessage(entry); err != nil {
575+
logger.Error("Failed to add optimized message", "error", err)
567576
}
568577
}
569578

570-
reduction := len(messages) - len(optimizedMessages)
571-
reductionPercent := (float64(reduction) / float64(len(messages))) * 100
572-
573-
infoEntry := domain.ConversationEntry{
574-
Message: sdk.Message{
575-
Role: sdk.Assistant,
576-
Content: sdk.NewMessageContent(fmt.Sprintf("Conversation compacted successfully! Reduced from %d to %d messages (%.1f%% reduction).", len(messages), len(optimizedMessages), reductionPercent)),
577-
},
578-
Model: "",
579-
Time: time.Now(),
580-
}
581-
582-
if addErr := s.handler.conversationRepo.AddMessage(infoEntry); addErr != nil {
583-
logger.Error("failed to add compact info message", "error", addErr)
584-
}
585-
586579
return tea.Batch(
587580
func() tea.Msg {
588581
return domain.UpdateHistoryEvent{
@@ -591,7 +584,7 @@ func (s *ChatShortcutHandler) performCompactAsync() tea.Cmd {
591584
},
592585
func() tea.Msg {
593586
return domain.SetStatusEvent{
594-
Message: fmt.Sprintf("Conversation compacted: %d messages reduced to %d", len(messages), len(optimizedMessages)),
587+
Message: fmt.Sprintf("• Started new conversation with summary (%d messages preserved)", len(messages)),
595588
Spinner: false,
596589
StatusType: domain.StatusDefault,
597590
}

internal/infra/adapters/persistent_conversation_adapter.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ func (a *PersistentConversationAdapter) GetCurrentConversationMetadata() shortcu
7171
CostStats: metadata.CostStats,
7272
Model: metadata.Model,
7373
Tags: metadata.Tags,
74-
Summary: metadata.Summary,
7574
}
7675
}
7776

internal/infra/storage/interfaces.go

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,19 @@ type ConversationStorage interface {
3636

3737
// ConversationMetadata contains metadata about a conversation
3838
type ConversationMetadata struct {
39-
ID string `json:"id"`
40-
Title string `json:"title"`
41-
CreatedAt time.Time `json:"created_at"`
42-
UpdatedAt time.Time `json:"updated_at"`
43-
MessageCount int `json:"message_count"`
44-
TokenStats domain.SessionTokenStats `json:"token_stats"`
45-
CostStats domain.SessionCostStats `json:"cost_stats,omitempty"`
46-
Model string `json:"model,omitempty"`
47-
Tags []string `json:"tags,omitempty"`
48-
Summary string `json:"summary,omitempty"`
49-
OptimizedMessages []domain.ConversationEntry `json:"optimized_messages,omitempty"`
50-
TitleGenerated bool `json:"title_generated,omitempty"`
51-
TitleInvalidated bool `json:"title_invalidated,omitempty"`
52-
TitleGenerationTime *time.Time `json:"title_generation_time,omitempty"`
53-
ContextID string `json:"context_id,omitempty"`
39+
ID string `json:"id"`
40+
Title string `json:"title"`
41+
CreatedAt time.Time `json:"created_at"`
42+
UpdatedAt time.Time `json:"updated_at"`
43+
MessageCount int `json:"message_count"`
44+
TokenStats domain.SessionTokenStats `json:"token_stats"`
45+
CostStats domain.SessionCostStats `json:"cost_stats,omitempty"`
46+
Model string `json:"model,omitempty"`
47+
Tags []string `json:"tags,omitempty"`
48+
TitleGenerated bool `json:"title_generated,omitempty"`
49+
TitleInvalidated bool `json:"title_invalidated,omitempty"`
50+
TitleGenerationTime *time.Time `json:"title_generation_time,omitempty"`
51+
ContextID string `json:"context_id,omitempty"`
5452
}
5553

5654
// ConversationSummary contains summary information about a conversation

internal/infra/storage/jsonl.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,6 @@ func (s *JsonlStorage) ListConversations(ctx context.Context, limit, offset int)
208208
CostStats: metadataWrapper.Metadata.CostStats,
209209
Model: metadataWrapper.Metadata.Model,
210210
Tags: metadataWrapper.Metadata.Tags,
211-
Summary: metadataWrapper.Metadata.Summary,
212211
TitleGenerated: metadataWrapper.Metadata.TitleGenerated,
213212
TitleInvalidated: metadataWrapper.Metadata.TitleInvalidated,
214213
TitleGenerationTime: metadataWrapper.Metadata.TitleGenerationTime,

internal/infra/storage/memory.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ func (m *MemoryStorage) ListConversations(ctx context.Context, limit, offset int
8181
TokenStats: data.metadata.TokenStats,
8282
Model: data.metadata.Model,
8383
Tags: data.metadata.Tags,
84-
Summary: data.metadata.Summary,
8584
TitleGenerated: data.metadata.TitleGenerated,
8685
TitleInvalidated: data.metadata.TitleInvalidated,
8786
TitleGenerationTime: data.metadata.TitleGenerationTime,

internal/infra/storage/migrations/postgres_migrations.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ func GetPostgresMigrations() []Migration {
1515
message_count INTEGER NOT NULL DEFAULT 0,
1616
model VARCHAR(255),
1717
tags JSONB,
18-
summary TEXT,
19-
optimized_messages JSONB,
2018
token_stats JSONB,
2119
cost_stats JSONB,
2220
title_generated BOOLEAN DEFAULT FALSE,

internal/infra/storage/migrations/sqlite_migrations.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,12 @@ func GetSQLiteMigrations() []Migration {
1212
title TEXT NOT NULL,
1313
count INTEGER NOT NULL DEFAULT 0,
1414
messages TEXT NOT NULL,
15-
optimized_messages TEXT,
1615
total_input_tokens INTEGER NOT NULL DEFAULT 0,
1716
total_output_tokens INTEGER NOT NULL DEFAULT 0,
1817
request_count INTEGER NOT NULL DEFAULT 0,
1918
cost_stats TEXT DEFAULT '{}',
2019
models TEXT DEFAULT '[]',
2120
tags TEXT DEFAULT '[]',
22-
summary TEXT DEFAULT '',
2321
title_generated BOOLEAN DEFAULT FALSE,
2422
title_invalidated BOOLEAN DEFAULT FALSE,
2523
title_generation_time DATETIME,

internal/infra/storage/postgres.go

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -130,31 +130,21 @@ func (s *PostgresStorage) SaveConversation(ctx context.Context, conversationID s
130130
return fmt.Errorf("failed to marshal cost stats: %w", err)
131131
}
132132

133-
var optimizedMessagesJSON []byte
134-
if len(metadata.OptimizedMessages) > 0 {
135-
optimizedMessagesJSON, err = json.Marshal(metadata.OptimizedMessages)
136-
if err != nil {
137-
return fmt.Errorf("failed to marshal optimized messages: %w", err)
138-
}
139-
}
140-
141133
_, err = tx.ExecContext(ctx, `
142-
INSERT INTO conversations (id, title, created_at, updated_at, message_count, model, tags, summary, optimized_messages, token_stats, cost_stats, title_generated, title_invalidated, title_generation_time)
143-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
134+
INSERT INTO conversations (id, title, created_at, updated_at, message_count, model, tags, token_stats, cost_stats, title_generated, title_invalidated, title_generation_time)
135+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
144136
ON CONFLICT(id) DO UPDATE SET
145137
title = EXCLUDED.title,
146138
updated_at = EXCLUDED.updated_at,
147139
message_count = EXCLUDED.message_count,
148140
model = EXCLUDED.model,
149141
tags = EXCLUDED.tags,
150-
summary = EXCLUDED.summary,
151-
optimized_messages = EXCLUDED.optimized_messages,
152142
token_stats = EXCLUDED.token_stats,
153143
cost_stats = EXCLUDED.cost_stats,
154144
title_generated = EXCLUDED.title_generated,
155145
title_invalidated = EXCLUDED.title_invalidated,
156146
title_generation_time = EXCLUDED.title_generation_time
157-
`, conversationID, metadata.Title, metadata.CreatedAt, metadata.UpdatedAt, len(entries), metadata.Model, string(tagsJSON), metadata.Summary, optimizedMessagesJSON, string(tokenStatsJSON), string(costStatsJSON), metadata.TitleGenerated, metadata.TitleInvalidated, metadata.TitleGenerationTime)
147+
`, conversationID, metadata.Title, metadata.CreatedAt, metadata.UpdatedAt, len(entries), metadata.Model, string(tagsJSON), string(tokenStatsJSON), string(costStatsJSON), metadata.TitleGenerated, metadata.TitleInvalidated, metadata.TitleGenerationTime)
158148
if err != nil {
159149
return fmt.Errorf("failed to save conversation metadata: %w", err)
160150
}
@@ -186,15 +176,14 @@ func (s *PostgresStorage) SaveConversation(ctx context.Context, conversationID s
186176
func (s *PostgresStorage) LoadConversation(ctx context.Context, conversationID string) ([]domain.ConversationEntry, ConversationMetadata, error) {
187177
var metadata ConversationMetadata
188178
var tokenStatsJSON, tagsJSON, costStatsJSON string
189-
var optimizedMessagesJSON sql.NullString
190179

191180
err := s.db.QueryRowContext(ctx, `
192-
SELECT id, title, created_at, updated_at, message_count, model, tags, summary, optimized_messages, token_stats, COALESCE(cost_stats, '{}'),
181+
SELECT id, title, created_at, updated_at, message_count, model, tags, token_stats, COALESCE(cost_stats, '{}'),
193182
COALESCE(title_generated, FALSE), COALESCE(title_invalidated, FALSE), title_generation_time
194183
FROM conversations WHERE id = $1
195184
`, conversationID).Scan(
196185
&metadata.ID, &metadata.Title, &metadata.CreatedAt, &metadata.UpdatedAt,
197-
&metadata.MessageCount, &metadata.Model, &tagsJSON, &metadata.Summary, &optimizedMessagesJSON, &tokenStatsJSON, &costStatsJSON,
186+
&metadata.MessageCount, &metadata.Model, &tagsJSON, &tokenStatsJSON, &costStatsJSON,
198187
&metadata.TitleGenerated, &metadata.TitleInvalidated, &metadata.TitleGenerationTime,
199188
)
200189
if err != nil {
@@ -218,12 +207,6 @@ func (s *PostgresStorage) LoadConversation(ctx context.Context, conversationID s
218207
return nil, metadata, fmt.Errorf("failed to unmarshal tags: %w", err)
219208
}
220209

221-
if optimizedMessagesJSON.Valid && optimizedMessagesJSON.String != "" {
222-
if err := json.Unmarshal([]byte(optimizedMessagesJSON.String), &metadata.OptimizedMessages); err != nil {
223-
return nil, metadata, fmt.Errorf("failed to unmarshal optimized messages: %w", err)
224-
}
225-
}
226-
227210
rows, err := s.db.QueryContext(ctx, `
228211
SELECT entry_data FROM conversation_entries
229212
WHERE conversation_id = $1
@@ -273,7 +256,7 @@ func (s *PostgresStorage) ListConversations(ctx context.Context, limit, offset i
273256

274257
err := rows.Scan(
275258
&summary.ID, &summary.Title, &summary.CreatedAt, &summary.UpdatedAt,
276-
&summary.MessageCount, &summary.Model, &tagsJSON, &summary.Summary, &tokenStatsJSON, &costStatsJSON,
259+
&summary.MessageCount, &summary.Model, &tagsJSON, &tokenStatsJSON, &costStatsJSON,
277260
&summary.TitleGenerated, &summary.TitleInvalidated, &summary.TitleGenerationTime,
278261
)
279262
if err != nil {
@@ -323,7 +306,7 @@ func (s *PostgresStorage) ListConversationsNeedingTitles(ctx context.Context, li
323306

324307
err := rows.Scan(
325308
&summary.ID, &summary.Title, &summary.CreatedAt, &summary.UpdatedAt,
326-
&summary.MessageCount, &summary.Model, &tagsJSON, &summary.Summary, &tokenStatsJSON, &costStatsJSON,
309+
&summary.MessageCount, &summary.Model, &tagsJSON, &tokenStatsJSON, &costStatsJSON,
327310
&summary.TitleGenerated, &summary.TitleInvalidated, &summary.TitleGenerationTime,
328311
)
329312
if err != nil {
@@ -388,10 +371,10 @@ func (s *PostgresStorage) UpdateConversationMetadata(ctx context.Context, conver
388371

389372
result, err := s.db.ExecContext(ctx, `
390373
UPDATE conversations
391-
SET title = $1, updated_at = $2, model = $3, tags = $4, summary = $5, token_stats = $6, cost_stats = $7,
392-
title_generated = $8, title_invalidated = $9, title_generation_time = $10
393-
WHERE id = $11
394-
`, metadata.Title, metadata.UpdatedAt, metadata.Model, string(tagsJSON), metadata.Summary, string(tokenStatsJSON), string(costStatsJSON), metadata.TitleGenerated, metadata.TitleInvalidated, metadata.TitleGenerationTime, conversationID)
374+
SET title = $1, updated_at = $2, model = $3, tags = $4, token_stats = $5, cost_stats = $6,
375+
title_generated = $7, title_invalidated = $8, title_generation_time = $9
376+
WHERE id = $10
377+
`, metadata.Title, metadata.UpdatedAt, metadata.Model, string(tagsJSON), string(tokenStatsJSON), string(costStatsJSON), metadata.TitleGenerated, metadata.TitleInvalidated, metadata.TitleGenerationTime, conversationID)
395378
if err != nil {
396379
return fmt.Errorf("failed to update conversation metadata: %w", err)
397380
}

internal/infra/storage/redis.go

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -221,15 +221,15 @@ func (s *RedisStorage) ListConversations(ctx context.Context, limit, offset int)
221221
}
222222

223223
summary := ConversationSummary{
224-
ID: metadata.ID,
225-
Title: metadata.Title,
226-
CreatedAt: metadata.CreatedAt,
227-
UpdatedAt: metadata.UpdatedAt,
228-
MessageCount: metadata.MessageCount,
229-
TokenStats: metadata.TokenStats,
230-
Model: metadata.Model,
231-
Tags: metadata.Tags,
232-
Summary: metadata.Summary,
224+
ID: metadata.ID,
225+
Title: metadata.Title,
226+
CreatedAt: metadata.CreatedAt,
227+
UpdatedAt: metadata.UpdatedAt,
228+
MessageCount: metadata.MessageCount,
229+
TokenStats: metadata.TokenStats,
230+
Model: metadata.Model,
231+
Tags: metadata.Tags,
232+
233233
TitleGenerated: metadata.TitleGenerated,
234234
TitleInvalidated: metadata.TitleInvalidated,
235235
TitleGenerationTime: metadata.TitleGenerationTime,
@@ -288,15 +288,15 @@ func (s *RedisStorage) ListConversationsNeedingTitles(ctx context.Context, limit
288288

289289
if (!metadata.TitleGenerated || metadata.TitleInvalidated) && metadata.MessageCount > 0 {
290290
summary := ConversationSummary{
291-
ID: metadata.ID,
292-
Title: metadata.Title,
293-
CreatedAt: metadata.CreatedAt,
294-
UpdatedAt: metadata.UpdatedAt,
295-
MessageCount: metadata.MessageCount,
296-
TokenStats: metadata.TokenStats,
297-
Model: metadata.Model,
298-
Tags: metadata.Tags,
299-
Summary: metadata.Summary,
291+
ID: metadata.ID,
292+
Title: metadata.Title,
293+
CreatedAt: metadata.CreatedAt,
294+
UpdatedAt: metadata.UpdatedAt,
295+
MessageCount: metadata.MessageCount,
296+
TokenStats: metadata.TokenStats,
297+
Model: metadata.Model,
298+
Tags: metadata.Tags,
299+
300300
TitleGenerated: metadata.TitleGenerated,
301301
TitleInvalidated: metadata.TitleInvalidated,
302302
TitleGenerationTime: metadata.TitleGenerationTime,

0 commit comments

Comments
 (0)