From 1866d48cb2c1c6c40fefb3ad1b021e54513dfb28 Mon Sep 17 00:00:00 2001 From: PeninsulaInd Date: Sat, 14 Feb 2026 14:23:33 -0600 Subject: [PATCH] Fix chat memory persistence, duplicate tool calls, and heartbeat log noise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Agent: deduplicate tool calls across iterations, reduce max iterations 10→5, add system prompt instructions to prevent re-calling tools - Router: preserve tool name in history messages, add anti-loop and delegate_task instructions to system prompt - Memory: auto_flush now deletes flushed messages from DB so conversations don't get re-summarized repeatedly, skip tool results in summaries - DB: add delete_messages() method, include message id in get_messages() - Scheduler: stop logging routine heartbeat checks to daily log Co-Authored-By: Claude Opus 4.6 --- cheddahbot/agent.py | 30 +++++++++++++++++++++++++----- cheddahbot/db.py | 12 +++++++++++- cheddahbot/memory.py | 34 +++++++++++++++++++++++++--------- cheddahbot/router.py | 11 +++++++++-- cheddahbot/scheduler.py | 3 --- 5 files changed, 70 insertions(+), 20 deletions(-) diff --git a/cheddahbot/agent.py b/cheddahbot/agent.py index 39fee24..30bf4f1 100644 --- a/cheddahbot/agent.py +++ b/cheddahbot/agent.py @@ -14,7 +14,7 @@ from .router import build_system_prompt, format_messages_for_llm log = logging.getLogger(__name__) -MAX_TOOL_ITERATIONS = 10 +MAX_TOOL_ITERATIONS = 5 class Agent: @@ -72,6 +72,8 @@ class Agent: messages = format_messages_for_llm(system_prompt, history, self.config.memory.max_context_messages) # Agent loop: LLM call → tool execution → repeat + seen_tool_calls: set[str] = set() # track (name, args_json) to prevent duplicates + for iteration in range(MAX_TOOL_ITERATIONS): full_response = "" tool_calls = [] @@ -89,10 +91,28 @@ class Agent: self.db.add_message(conv_id, "assistant", full_response, model=self.llm.current_model) break + # Filter out duplicate tool calls + unique_tool_calls = [] + for tc in tool_calls: + call_key = f"{tc['name']}:{json.dumps(tc.get('input', {}), sort_keys=True)}" + if call_key in seen_tool_calls: + log.warning("Skipping duplicate tool call: %s", tc["name"]) + continue + seen_tool_calls.add(call_key) + unique_tool_calls.append(tc) + + if not unique_tool_calls: + # All tool calls were duplicates — force the model to respond + if full_response: + self.db.add_message(conv_id, "assistant", full_response, model=self.llm.current_model) + else: + yield "(I already have the information needed to answer.)" + break + # Store assistant message with tool calls self.db.add_message( conv_id, "assistant", full_response, - tool_calls=[{"name": tc["name"], "input": tc["input"]} for tc in tool_calls], + tool_calls=[{"name": tc["name"], "input": tc["input"]} for tc in unique_tool_calls], model=self.llm.current_model, ) @@ -100,8 +120,8 @@ class Agent: if self._tools: messages.append({"role": "assistant", "content": full_response or "I'll use some tools to help with that."}) - for tc in tool_calls: - yield f"\n\nšŸ”§ **Using tool: {tc['name']}**\n" + for tc in unique_tool_calls: + yield f"\n\n**Using tool: {tc['name']}**\n" try: result = self._tools.execute(tc["name"], tc.get("input", {})) except Exception as e: @@ -114,7 +134,7 @@ class Agent: # No tool system configured - just mention tool was requested if full_response: self.db.add_message(conv_id, "assistant", full_response, model=self.llm.current_model) - for tc in tool_calls: + for tc in unique_tool_calls: yield f"\n(Tool requested: {tc['name']} - tool system not yet initialized)\n" break else: diff --git a/cheddahbot/db.py b/cheddahbot/db.py index cc06ea7..9df2ba3 100644 --- a/cheddahbot/db.py +++ b/cheddahbot/db.py @@ -119,7 +119,7 @@ class Database: def get_messages(self, conv_id: str, limit: int = 100) -> list[dict]: rows = self._conn.execute( - """SELECT role, content, tool_calls, tool_result, model, created_at + """SELECT id, role, content, tool_calls, tool_result, model, created_at FROM messages WHERE conv_id = ? ORDER BY created_at ASC LIMIT ?""", (conv_id, limit), ).fetchall() @@ -137,6 +137,16 @@ class Database: ).fetchone() return row["cnt"] + def delete_messages(self, message_ids: list[int]): + """Delete messages by their IDs (used by auto_flush).""" + if not message_ids: + return + placeholders = ",".join("?" for _ in message_ids) + self._conn.execute( + f"DELETE FROM messages WHERE id IN ({placeholders})", message_ids + ) + self._conn.commit() + # -- Scheduled Tasks -- def add_scheduled_task(self, name: str, prompt: str, schedule: str) -> int: diff --git a/cheddahbot/memory.py b/cheddahbot/memory.py index 62e4a1b..57e11b8 100644 --- a/cheddahbot/memory.py +++ b/cheddahbot/memory.py @@ -99,21 +99,37 @@ class MemorySystem: return self._vector_search(query_vec, top_k) def auto_flush(self, conv_id: str): - """Summarize old messages and move to daily log.""" + """Summarize old messages and move to daily log, then delete flushed messages.""" messages = self.db.get_messages(conv_id, limit=200) if len(messages) < self.config.memory.flush_threshold: return - # Take older messages for summarization - to_summarize = messages[:-10] # keep last 10 in context - text_block = "\n".join( - f"{m['role']}: {m['content'][:200]}" for m in to_summarize - if m.get("content") - ) + # Take older messages for summarization, keep last 10 in context + to_summarize = messages[:-10] + if not to_summarize: + return - summary = f"Conversation summary ({len(to_summarize)} messages): {text_block[:1000]}" + # Build a concise summary (skip tool results, keep user/assistant text) + summary_parts = [] + for m in to_summarize: + role = m.get("role", "") + content = (m.get("content") or "").strip() + if not content or role == "tool": + continue + summary_parts.append(f"{role}: {content[:150]}") + + if not summary_parts: + return + + summary = f"Conversation summary ({len(to_summarize)} messages):\n" + "\n".join(summary_parts[:20]) self.log_daily(summary) - log.info("Auto-flushed %d messages to daily log", len(to_summarize)) + + # Delete the flushed messages from DB so they don't get re-flushed + flushed_ids = [m["id"] for m in to_summarize if "id" in m] + if flushed_ids: + self.db.delete_messages(flushed_ids) + + log.info("Auto-flushed %d messages from conv %s", len(to_summarize), conv_id) def reindex_all(self): """Rebuild the embedding index from all memory files.""" diff --git a/cheddahbot/router.py b/cheddahbot/router.py index f3f6656..1fff7a3 100644 --- a/cheddahbot/router.py +++ b/cheddahbot/router.py @@ -38,7 +38,13 @@ def build_system_prompt( "- If you learn something important about the user, save it to memory.\n" "- Be concise but thorough. Don't pad responses unnecessarily.\n" "- When uncertain, ask for clarification.\n" - "- Reference memories naturally when relevant." + "- Reference memories naturally when relevant.\n" + "- IMPORTANT: Do NOT call the same tool twice with the same arguments. " + "If a tool already returned a result, use that result — do not re-call it.\n" + "- After using tools, always respond to the user with a final answer. " + "Do not end your turn with only tool calls and no text.\n" + "- For tasks requiring shell commands, file edits, or system access, " + "use the delegate_task tool instead of trying to do it yourself." ) return "\n\n---\n\n".join(parts) @@ -62,6 +68,7 @@ def format_messages_for_llm( messages.append({"role": role, "content": content}) elif role == "tool": # Tool results go as a user message with context - messages.append({"role": "user", "content": f"[Tool Result]\n{content}"}) + tool_name = msg.get("tool_result", "unknown") + messages.append({"role": "user", "content": f'[Tool "{tool_name}" result]\n{content}'}) return messages diff --git a/cheddahbot/scheduler.py b/cheddahbot/scheduler.py index fdd9fd7..de108e4 100644 --- a/cheddahbot/scheduler.py +++ b/cheddahbot/scheduler.py @@ -113,6 +113,3 @@ class Scheduler: log.debug("Heartbeat: all clear") else: log.info("Heartbeat action taken: %s", result[:200]) - # Log to daily log - if self.agent._memory: - self.agent._memory.log_daily(f"[Heartbeat] {result[:500]}")