fix(agent): improve memory recall accuracy and flush safety

- Add "low confidence" instruction to memory_search tool description
  to prevent models from fabricating memories when no results found
- Add dedicated ## Memory Recall section in system prompt (supplements
  recency reminder) with clear instructions for memory_search/memory_get
- Update flush prompts: replace YYYY-MM-DD with actual date at runtime,
  cleaner append-only wording
- Update AGENTS.md memory privacy section for multi-tenant: remove
  implementation details (per-user scoping), keep group chat output
  guardrails that work for both shared and isolated memory configs
This commit is contained in:
viettranx committed 2026-04-01 09:16:16 +07:00
1 parent 5b66d64545
commit 9c2b4cbf0f
5 files changed
+54 -11

No files matched your search

+13 -5
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log/slog"
"strings"
"time"
"github.com/nextlevelbuilder/goclaw/internal/config"
@@ -15,12 +16,14 @@ import (
// Default memory flush prompts matching TS memory-flush.ts.
const (
DefaultMemoryFlushPrompt = "Pre-compaction memory flush. " +
"Store durable memories now (use memory/YYYY-MM-DD.md; create memory/ if needed). " +
"IMPORTANT: If the file already exists, APPEND new content only and do not overwrite existing entries. " +
"Append durable memories to memory/YYYY-MM-DD.md (create memory/ if needed). " +
"If the file already exists, APPEND only — do not overwrite existing entries. " +
"If nothing to store, reply with NO_REPLY."
DefaultMemoryFlushSystemPrompt = "Pre-compaction memory flush turn. " +
"The session is near auto-compaction; capture durable memories to disk. " +
"Append to memory/YYYY-MM-DD.md only. " +
"If the file already exists, append — do not overwrite. " +
"You may reply, but usually NO_REPLY is correct."
)
@@ -100,6 +103,11 @@ func (l *Loop) runMemoryFlush(ctx context.Context, sessionKey string, settings *
var messages []providers.Message
// Replace YYYY-MM-DD placeholder with today's date in flush prompts.
today := time.Now().Format("2006-01-02")
flushPrompt := strings.ReplaceAll(settings.Prompt, "YYYY-MM-DD", today)
flushSystemPrompt := strings.ReplaceAll(settings.SystemPrompt, "YYYY-MM-DD", today)
// System prompt: combine agent's normal system prompt context with flush system prompt
systemPrompt := BuildSystemPrompt(SystemPromptConfig{
AgentID: l.id,
@@ -110,7 +118,7 @@ func (l *Loop) runMemoryFlush(ctx context.Context, sessionKey string, settings *
HasMemory: l.hasMemory,
ProviderType: providerTypeOf(l.provider),
})
systemPrompt += "\n\n" + settings.SystemPrompt
systemPrompt += "\n\n" + flushSystemPrompt
messages = append(messages, providers.Message{
Role: "system",
@@ -137,10 +145,10 @@ func (l *Loop) runMemoryFlush(ctx context.Context, sessionKey string, settings *
sanitized, _ := sanitizeHistory(recentHistory)
messages = append(messages, sanitized...)
// Flush prompt
// Flush prompt (with YYYY-MM-DD replaced by today's date)
messages = append(messages, providers.Message{
Role: "user",
Content: settings.Prompt,
Content: flushPrompt,
})
// Build tool list — only file tools needed for memory flush
+9 -2
View File
@@ -3,6 +3,7 @@ package agent
import (
"fmt"
"log/slog"
"slices"
"strings"
"github.com/nextlevelbuilder/goclaw/internal/bootstrap"
@@ -312,6 +313,12 @@ func BuildSystemPrompt(cfg SystemPromptConfig) string {
lines = append(lines, buildProjectContextSection(otherFiles, cfg.AgentType)...)
}
// 12.5. ## Memory Recall — dedicated section (supplements recency reminder at end)
if !isMinimal && cfg.HasMemory {
hasMemoryGet := slices.Contains(cfg.ToolNames, "memory_get")
lines = append(lines, buildMemoryRecallSection(hasMemoryGet, cfg.HasKnowledgeGraph)...)
}
// 13. ## Sub-Agent Spawning — skipped for team agents and bootstrap
if !cfg.IsBootstrap && cfg.HasSpawn && !cfg.HasTeam {
lines = append(lines, buildSpawnSection()...)
@@ -329,9 +336,9 @@ func BuildSystemPrompt(cfg SystemPromptConfig) string {
lines = append(lines, "Reminder: Follow AGENTS.md rules — memory recall before answering, NO_REPLY when silent, match the user's language.", "")
}
if !isMinimal && cfg.HasMemory {
memReminder := "Reminder: Before answering questions about prior work, decisions, or preferences, always run memory_search first."
memReminder := "Reminder: Before answering questions about prior work, decisions, or preferences, always run memory_search first. If nothing relevant is found, say you checked but found nothing."
if cfg.HasKnowledgeGraph {
memReminder += " Also run knowledge_graph_search when the question involves people, teams, projects, or connections — it finds relationship paths that memory_search misses."
memReminder += " Also run knowledge_graph_search when the question involves people, teams, projects, or connections."
}
lines = append(lines, memReminder, "")
}
+27
View File
@@ -89,6 +89,33 @@ func buildSandboxSection(cfg SystemPromptConfig) []string {
return lines
}
// buildMemoryRecallSection generates the ## Memory Recall section for the system prompt.
// Supplements (does NOT replace) the recency reminder at the end of the prompt.
func buildMemoryRecallSection(hasMemoryGet, hasKG bool) []string {
lines := []string{"## Memory Recall", ""}
if hasMemoryGet {
lines = append(lines,
"Before answering questions about prior work, decisions, people, preferences, or todos: "+
"call memory_search with a relevant query; then use memory_get to pull only the needed lines. "+
"If no relevant results found, tell the user you checked but found nothing.")
} else {
lines = append(lines,
"Before answering questions about prior work, decisions, people, preferences, or todos: "+
"call memory_search with a relevant query and answer from the matching results. "+
"If no relevant results found, tell the user you checked but found nothing.")
}
if hasKG {
lines = append(lines,
"Also run knowledge_graph_search when the question involves people, teams, projects, or connections — "+
"it finds multi-hop relationship paths that memory_search misses.")
}
lines = append(lines, "")
return lines
}
func buildUserIdentitySection(ownerIDs []string) []string {
return []string{
"## User Identity",
+4 -3
View File
@@ -33,10 +33,11 @@ You start fresh each session. Use tools to maintain continuity:
If `knowledge_graph_search` is available, also run it for questions about people, teams, projects, or connections — it finds multi-hop relationships that `memory_search` misses.
- When asked to save or remember something, you MUST call a write tool (`write_file` or `edit`) in THIS turn. Never claim "already saved" without a tool call.
### MEMORY.md Privacy
### Memory Privacy
- Only reference MEMORY.md content in **private/direct chats** with your user
- In group chats or shared sessions, do NOT surface personal memory content
- In group chats: do NOT surface personal memory content (private preferences, personal history)
- Memory details (file paths, raw notes) should only be shared in **private/direct chats**
- In groups, use memory to inform your answers, but don't quote or reference it directly
## Group Chats
+1 -1
View File
@@ -34,7 +34,7 @@ func (t *MemorySearchTool) SetHasKG(has bool) {
func (t *MemorySearchTool) Name() string { return "memory_search" }
func (t *MemorySearchTool) Description() string {
return "Mandatory recall step: semantically search MEMORY.md + memory/*.md before answering questions about prior work, decisions, dates, people, preferences, or todos; returns top snippets with path + lines. If response has disabled=true, memory retrieval is unavailable and should be surfaced to the user. IMPORTANT: Always query in the SAME language as the stored memory content. If the user speaks Vietnamese, search in Vietnamese. If memory was written in English, search in English. Matching the language dramatically improves search accuracy."
return "Mandatory recall step: semantically search MEMORY.md + memory/*.md before answering questions about prior work, decisions, dates, people, preferences, or todos; returns top snippets with path + lines. If response has disabled=true, memory retrieval is unavailable and should be surfaced to the user. IMPORTANT: Always query in the SAME language as the stored memory content. If the user speaks Vietnamese, search in Vietnamese. If memory was written in English, search in English. Matching the language dramatically improves search accuracy. If no relevant results found or confidence is low, tell the user you checked but found nothing — do not fabricate or guess memories."
}
func (t *MemorySearchTool) Parameters() map[string]any {