diff --git a/internal/agent/memoryflush.go b/internal/agent/memoryflush.go index 28197087..7faa8a8a 100644 --- a/internal/agent/memoryflush.go +++ b/internal/agent/memoryflush.go @@ -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 diff --git a/internal/agent/systemprompt.go b/internal/agent/systemprompt.go index 7da0676e..8434d855 100644 --- a/internal/agent/systemprompt.go +++ b/internal/agent/systemprompt.go @@ -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, "") } diff --git a/internal/agent/systemprompt_sections.go b/internal/agent/systemprompt_sections.go index 77f837d7..96545e4f 100644 --- a/internal/agent/systemprompt_sections.go +++ b/internal/agent/systemprompt_sections.go @@ -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", diff --git a/internal/bootstrap/templates/AGENTS.md b/internal/bootstrap/templates/AGENTS.md index 4f452478..0aa367c9 100644 --- a/internal/bootstrap/templates/AGENTS.md +++ b/internal/bootstrap/templates/AGENTS.md @@ -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 diff --git a/internal/tools/memory.go b/internal/tools/memory.go index 7cd8ded0..6573aa5e 100644 --- a/internal/tools/memory.go +++ b/internal/tools/memory.go @@ -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 {