This commit is contained in:
lpf
2026-02-23 16:38:00 +08:00
parent 95e9be18b8
commit b5430b9021
13 changed files with 1197 additions and 119 deletions

View File

@@ -11,12 +11,21 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"time"
"clawgo/pkg/config"
"clawgo/pkg/logger"
)
const (
maxMemoryContextChars = 6000
maxLongTermMemoryChars = 2200
maxRecentNotesChars = 1600
maxMemoryLayerPartChars = 1200
maxMemoryDigestLines = 14
)
// MemoryStore manages persistent memory for the agent.
// - Long-term memory: memory/MEMORY.md
// - Daily notes: memory/YYYYMM/YYYYMMDD.md
@@ -29,6 +38,7 @@ type MemoryStore struct {
includeProfile bool
includeProject bool
includeProcedure bool
mu sync.Mutex
}
// NewMemoryStore creates a new MemoryStore with the given workspace path.
@@ -120,10 +130,13 @@ func (ms *MemoryStore) ReadLongTerm() string {
// WriteLongTerm writes content to the long-term memory file (MEMORY.md).
func (ms *MemoryStore) WriteLongTerm(content string) error {
ms.mu.Lock()
defer ms.mu.Unlock()
if err := os.MkdirAll(ms.memoryDir, 0755); err != nil {
return err
}
return os.WriteFile(ms.memoryFile, []byte(content), 0644)
return atomicWriteFile(ms.memoryFile, []byte(content), 0644)
}
// ReadToday reads today's daily note.
@@ -139,6 +152,9 @@ func (ms *MemoryStore) ReadToday() string {
// AppendToday appends content to today's daily note.
// If the file doesn't exist, it creates a new file with a date header.
func (ms *MemoryStore) AppendToday(content string) error {
ms.mu.Lock()
defer ms.mu.Unlock()
todayFile := ms.getTodayFile()
// Ensure month directory exists
@@ -147,22 +163,28 @@ func (ms *MemoryStore) AppendToday(content string) error {
return err
}
var existingContent string
if data, err := os.ReadFile(todayFile); err == nil {
existingContent = string(data)
f, err := os.OpenFile(todayFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return err
}
var newContent string
if existingContent == "" {
payload := content
if info.Size() == 0 {
// Add header for new day
header := fmt.Sprintf("# %s\n\n", time.Now().Format("2006-01-02"))
newContent = header + content
payload = header + content
} else {
// Append to existing content
newContent = existingContent + "\n" + content
payload = "\n" + content
}
return os.WriteFile(todayFile, []byte(newContent), 0644)
_, err = f.WriteString(payload)
return err
}
// GetRecentDailyNotes returns daily notes from the last N days.
@@ -209,13 +231,13 @@ func (ms *MemoryStore) GetMemoryContext() string {
// Long-term memory
longTerm := ms.ReadLongTerm()
if longTerm != "" {
parts = append(parts, "## Long-term Memory\n\n"+longTerm)
parts = append(parts, "## Long-term Memory (Digest)\n\n"+compressMemoryForPrompt(longTerm, maxMemoryDigestLines, maxLongTermMemoryChars))
}
// Recent daily notes
recentNotes := ms.GetRecentDailyNotes(ms.recentDays)
if recentNotes != "" {
parts = append(parts, "## Recent Daily Notes\n\n"+recentNotes)
parts = append(parts, "## Recent Daily Notes (Digest)\n\n"+compressMemoryForPrompt(recentNotes, maxMemoryDigestLines, maxRecentNotesChars))
}
if len(parts) == 0 {
@@ -230,7 +252,7 @@ func (ms *MemoryStore) GetMemoryContext() string {
}
result += part
}
return fmt.Sprintf("# Memory\n\n%s", result)
return fmt.Sprintf("# Memory\n\n%s", truncateMemoryText(result, maxMemoryContextChars))
}
func (ms *MemoryStore) getLayeredContext() []string {
@@ -244,7 +266,7 @@ func (ms *MemoryStore) getLayeredContext() []string {
if strings.TrimSpace(content) == "" {
return
}
parts = append(parts, fmt.Sprintf("## %s\n\n%s", title, content))
parts = append(parts, fmt.Sprintf("## %s (Digest)\n\n%s", title, compressMemoryForPrompt(content, maxMemoryDigestLines, maxMemoryLayerPartChars)))
}
if ms.includeProfile {
@@ -258,3 +280,94 @@ func (ms *MemoryStore) getLayeredContext() []string {
}
return parts
}
func truncateMemoryText(content string, maxChars int) string {
if maxChars <= 0 {
return strings.TrimSpace(content)
}
trimmed := strings.TrimSpace(content)
runes := []rune(trimmed)
if len(runes) <= maxChars {
return trimmed
}
suffix := "\n\n...[truncated]"
suffixRunes := []rune(suffix)
if maxChars <= len(suffixRunes) {
return string(runes[:maxChars])
}
return strings.TrimSpace(string(runes[:maxChars-len(suffixRunes)])) + suffix
}
func compressMemoryForPrompt(content string, maxLines, maxChars int) string {
trimmed := strings.TrimSpace(content)
if trimmed == "" {
return ""
}
if maxLines <= 0 {
maxLines = maxMemoryDigestLines
}
lines := strings.Split(trimmed, "\n")
kept := make([]string, 0, maxLines)
inParagraph := false
for _, raw := range lines {
line := strings.TrimSpace(raw)
if line == "" {
inParagraph = false
continue
}
isHeading := strings.HasPrefix(line, "#")
isBullet := strings.HasPrefix(line, "- ") || strings.HasPrefix(line, "* ")
isNumbered := isNumberedListLine(line)
if isHeading || isBullet || isNumbered {
kept = append(kept, line)
inParagraph = false
} else if !inParagraph {
// Keep only the first line of each paragraph to form a compact digest.
kept = append(kept, line)
inParagraph = true
}
if len(kept) >= maxLines {
break
}
}
if len(kept) == 0 {
for _, raw := range lines {
line := strings.TrimSpace(raw)
if line == "" {
continue
}
kept = append(kept, line)
if len(kept) >= maxLines {
break
}
}
}
return truncateMemoryText(strings.Join(kept, "\n"), maxChars)
}
func isNumberedListLine(line string) bool {
dot := strings.Index(line, ".")
if dot <= 0 || dot >= len(line)-1 {
return false
}
for i := 0; i < dot; i++ {
if line[i] < '0' || line[i] > '9' {
return false
}
}
return line[dot+1] == ' '
}
func atomicWriteFile(path string, data []byte, perm os.FileMode) error {
tmpPath := path + ".tmp"
if err := os.WriteFile(tmpPath, data, perm); err != nil {
return err
}
return os.Rename(tmpPath, path)
}