feat: include user-specific daily notes in per-user memory context (#995)

This commit is contained in:
Rahul Bansal 2026-03-03 19:12:06 +05:30
parent 4bea9128d2
commit 127db43e0e
2 changed files with 82 additions and 11 deletions

View file

@ -448,9 +448,13 @@ func safePathSegment(v string) (string, bool) {
} }
// loadUserMemoryContext loads optional per-user memory content. // loadUserMemoryContext loads optional per-user memory content.
// Supported lookup order: // Supported lookup roots (in order):
// 1) <workspace>/users/<channel>/<chatID>/MEMORY.md // 1) <workspace>/users/<channel>/<chatID>
// 2) <workspace>/users/<chatID>/MEMORY.md // 2) <workspace>/users/<chatID>
//
// For each root, it reads:
// - MEMORY.md or memory/MEMORY.md (long-term)
// - memory/YYYYMM/YYYYMMDD.md for recent daily notes (last 3 days)
func (cb *ContextBuilder) loadUserMemoryContext(channel, chatID string) string { func (cb *ContextBuilder) loadUserMemoryContext(channel, chatID string) string {
channelSeg, okChannel := safePathSegment(channel) channelSeg, okChannel := safePathSegment(channel)
chatSeg, okChat := safePathSegment(chatID) chatSeg, okChat := safePathSegment(chatID)
@ -458,19 +462,58 @@ func (cb *ContextBuilder) loadUserMemoryContext(channel, chatID string) string {
return "" return ""
} }
candidates := []string{} roots := []string{}
if okChannel { if okChannel {
candidates = append(candidates, filepath.Join(cb.workspace, "users", channelSeg, chatSeg, "MEMORY.md")) roots = append(roots, filepath.Join(cb.workspace, "users", channelSeg, chatSeg))
} }
candidates = append(candidates, filepath.Join(cb.workspace, "users", chatSeg, "MEMORY.md")) roots = append(roots, filepath.Join(cb.workspace, "users", chatSeg))
for _, p := range candidates { for _, root := range roots {
if data, err := os.ReadFile(p); err == nil { longTerm := ""
text := strings.TrimSpace(string(data)) for _, longTermPath := range []string{filepath.Join(root, "MEMORY.md"), filepath.Join(root, "memory", "MEMORY.md")} {
if text != "" { if data, err := os.ReadFile(longTermPath); err == nil {
return "## User-specific Memory\n\n" + text text := strings.TrimSpace(string(data))
if text != "" {
longTerm = text
break
}
} }
} }
recentNotes := ""
memoryDir := filepath.Join(root, "memory")
for i := range 3 {
date := time.Now().AddDate(0, 0, -i).Format("20060102")
notePath := filepath.Join(memoryDir, date[:6], date+".md")
if data, err := os.ReadFile(notePath); err == nil {
text := strings.TrimSpace(string(data))
if text != "" {
if recentNotes != "" {
recentNotes += "\n\n---\n\n"
}
recentNotes += text
}
}
}
if longTerm == "" && recentNotes == "" {
continue
}
var sb strings.Builder
sb.WriteString("## User-specific Memory\n\n")
if longTerm != "" {
sb.WriteString("### Long-term Memory\n\n")
sb.WriteString(longTerm)
}
if recentNotes != "" {
if longTerm != "" {
sb.WriteString("\n\n")
}
sb.WriteString("### Recent Daily Notes\n\n")
sb.WriteString(recentNotes)
}
return sb.String()
} }
return "" return ""

View file

@ -701,3 +701,31 @@ func TestBuildMessages_UserSpecificMemoryIgnoredForInvalidChatID(t *testing.T) {
t.Fatalf("expected no user memory section for invalid chat id") t.Fatalf("expected no user memory section for invalid chat id")
} }
} }
func TestBuildMessages_IncludesUserMemoryFromMemoryDirAndDailyNotes(t *testing.T) {
today := time.Now().Format("20060102")
tmpDir := setupWorkspace(t, map[string]string{
"IDENTITY.md": "# Identity\nTest agent.",
"users/u42/memory/MEMORY.md": "timezone: UTC",
"users/u42/memory/" + today[:6] + "/" + today + ".md": "met a friend today",
})
defer os.RemoveAll(tmpDir)
cb := NewContextBuilder(tmpDir)
msgs := cb.BuildMessages(nil, "", "hello", nil, "webchat", "u42")
if len(msgs) == 0 || msgs[0].Role != "system" {
t.Fatalf("expected first system message")
}
if !strings.Contains(msgs[0].Content, "### Long-term Memory") {
t.Fatalf("expected long-term user memory heading")
}
if !strings.Contains(msgs[0].Content, "timezone: UTC") {
t.Fatalf("expected user long-term memory content")
}
if !strings.Contains(msgs[0].Content, "### Recent Daily Notes") {
t.Fatalf("expected user daily notes heading")
}
if !strings.Contains(msgs[0].Content, "met a friend today") {
t.Fatalf("expected user daily notes content")
}
}