Fix chat memory persistence, duplicate tool calls, and heartbeat log noise

- 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 <noreply@anthropic.com>
cora-start
PeninsulaInd 2026-02-14 14:23:33 -06:00
parent af767f9684
commit 1866d48cb2
5 changed files with 70 additions and 20 deletions

View File

@ -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:

View File

@ -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:

View File

@ -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."""

View File

@ -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

View File

@ -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]}")