From 33258ce2ad2712d47c89aaac0cc6e319e74818be Mon Sep 17 00:00:00 2001 From: opencode Date: Sun, 5 Jul 2026 07:03:20 +0000 Subject: [PATCH] fix: add sliding window to conversation history (issue #8) - Add _trim_conversation_history() with message count + token limits - Configurable via CLOVER_MAX_HISTORY_MESSAGES (default: 20) - Configurable via CLOVER_MAX_HISTORY_TOKENS (default: 8000) - Trims oldest messages first, preserves last 6 minimum - Prevents OOM and degraded LLM quality on long sessions --- ai_agent.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ config/settings.py | 4 ++++ 2 files changed, 50 insertions(+) diff --git a/ai_agent.py b/ai_agent.py index 0cbda1c..05f7630 100644 --- a/ai_agent.py +++ b/ai_agent.py @@ -34,6 +34,8 @@ class AIAgent: self.model_manager = ModelManager() self.conversation_history = [] self.available_tools = self._setup_tools() + self.max_history_messages = self.config.get("max_history_messages", 20) + self.max_history_tokens = self.config.get("max_history_tokens", 8000) def _setup_tools(self): """Setup available tools for the AI agent""" @@ -267,6 +269,47 @@ When using tools, be methodical and explain each step. Always test your creation except Exception as e: return {"error": f"Tool execution failed: {str(e)}"} + def _trim_conversation_history(self): + """ + Trim conversation history to prevent unbounded memory growth. + Uses a sliding window approach keeping the most recent messages. + + Preserves at least the last N messages (max_history_messages) + and stays within token budget (max_history_tokens). + """ + if len(self.conversation_history) <= self.max_history_messages: + return + + # Rough token estimation: ~4 chars per token + def estimate_tokens(msg): + return len(msg.get("content", "")) // 4 + + # Calculate total tokens in history + total_tokens = sum(estimate_tokens(msg) for msg in self.conversation_history) + + if total_tokens <= self.max_history_tokens: + # Within token budget, just apply message count limit + if len(self.conversation_history) > self.max_history_messages: + self.conversation_history = self.conversation_history[ + -self.max_history_messages : + ] + return + + # Over token budget - trim from the front, keeping recent messages + while ( + len(self.conversation_history) > 6 + and sum(estimate_tokens(msg) for msg in self.conversation_history) + > self.max_history_tokens + ): + # Remove pairs of messages (user + assistant) from the front + self.conversation_history = self.conversation_history[2:] + + # Also enforce message count limit + if len(self.conversation_history) > self.max_history_messages: + self.conversation_history = self.conversation_history[ + -self.max_history_messages : + ] + def chat(self, user_message: str) -> str: """ Have a conversation with the user, using tools as needed @@ -288,6 +331,9 @@ When using tools, be methodical and explain each step. Always test your creation # Prepare messages for AI messages = [{"role": "system", "content": self._create_system_prompt()}] + + # Trim history to prevent unbounded memory growth + self._trim_conversation_history() messages.extend(self.conversation_history) # Get AI response diff --git a/config/settings.py b/config/settings.py index 2a14913..20077c1 100644 --- a/config/settings.py +++ b/config/settings.py @@ -43,6 +43,10 @@ def load_config(): "project_root": os.getenv("CLOVER_PROJECT_ROOT", "."), "summary_file": os.getenv("CLOVER_SUMMARY_FILE", "clover.md"), "structure_file": os.getenv("CLOVER_STRUCTURE_FILE", "structure.md"), + "max_history_messages": int( + os.getenv("CLOVER_MAX_HISTORY_MESSAGES", "20") + ), + "max_history_tokens": int(os.getenv("CLOVER_MAX_HISTORY_TOKENS", "8000")), } # Debug output if enabled