-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmemory_context.go
More file actions
executable file
·153 lines (136 loc) · 5.2 KB
/
Copy pathmemory_context.go
File metadata and controls
executable file
·153 lines (136 loc) · 5.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package main
import (
"fmt"
"log"
"sort"
"strings"
"time"
)
// ContextMemory is a memory returned by GetContext, with its category.
type ContextMemory struct {
Category string `json:"category"`
Chunk MemoryChunk `json:"chunk"`
// AgeDays is whole days since the memory's effective time (UpdatedAt, else CreatedAt).
AgeDays int `json:"ageDays"`
// Stale is true when a time-sensitive category (see timeSensitiveTags) is older
// than staleAfterDays. Such memories describe point-in-time state and may no
// longer reflect reality.
Stale bool `json:"stale,omitempty"`
// StaleWarning is a human-readable caution, populated only when Stale is true,
// so a consuming agent treats the memory as possibly outdated rather than fact.
StaleWarning string `json:"staleWarning,omitempty"`
}
// ContextResult contains categorized memories for quick context restoration.
type ContextResult struct {
Memories []ContextMemory `json:"memories"`
}
// priorityTags defines which hashtags to look for and their priority order.
var priorityTags = []string{"#self", "#goal", "#relationship", "#status", "#principle", "#thought"}
// timeSensitiveTags are categories whose content describes point-in-time state
// (e.g. "current" project status) and therefore goes stale. For these, GetContext
// flags age so a consuming agent treats old entries with suspicion instead of fact.
// Durable categories (#self, #principle, #relationship) are intentionally excluded.
var timeSensitiveTags = map[string]bool{"#status": true}
// defaultPerCategory is the default number of memories returned per hashtag category.
const defaultPerCategory = 2
// staleAfterDays is how old a time-sensitive memory may be before it is flagged stale.
const staleAfterDays = 30
// GetContext returns important memories for context restoration.
// Looks for memories that START with priority hashtags (#self, #goal, #relationship, #status, #principle, #thought).
// Returns up to perCategory most recent memories per category, sorted by category priority.
// If perCategory is <= 0, the default (2) is used.
// If typeFilter is non-empty, only chunks with that type are considered.
// If scope is non-empty, only chunks matching that scope (or global) are considered.
func (s *MemoryStore) GetContext(typeFilter string, scope string, perCategory int) ContextResult {
if perCategory <= 0 {
perCategory = defaultPerCategory
}
log.Printf("MEMORY: CONTEXT type=%s scope=%s per_category=%d", typeFilter, scope, perCategory)
s.mu.RLock()
defer s.mu.RUnlock()
// Group memories by priority tag (must start with tag).
byTag := make(map[string][]*storedChunk)
for _, sc := range s.chunks {
if typeFilter != "" && sc.Type != typeFilter {
continue
}
if !matchesScope(sc.Scopes, scope) {
continue
}
textLower := strings.ToLower(sc.Text)
for _, tag := range priorityTags {
if strings.HasPrefix(textLower, tag) {
byTag[tag] = append(byTag[tag], sc)
break // Only first matching tag
}
}
}
// Sort each group by most-recent time (newest first) and take top N.
// Use UpdatedAt if set, otherwise CreatedAt — so recently updated memories
// (e.g. #status) surface first even if they were created long ago.
result := make([]ContextMemory, 0)
for _, tag := range priorityTags {
chunks := byTag[tag]
if len(chunks) == 0 {
continue
}
sort.Slice(chunks, func(i, j int) bool {
return effectiveTime(chunks[i]).After(effectiveTime(chunks[j]))
})
limit := perCategory
if len(chunks) < limit {
limit = len(chunks)
}
for i := 0; i < limit; i++ {
result = append(result, newContextMemory(tag, chunks[i]))
}
}
return ContextResult{Memories: result}
}
// newContextMemory builds a ContextMemory, stamping age and flagging staleness for
// time-sensitive categories so consumers can distinguish current state from old snapshots.
func newContextMemory(tag string, sc *storedChunk) ContextMemory {
ageDays := int(time.Since(effectiveTime(sc)).Hours() / 24)
if ageDays < 0 {
ageDays = 0
}
cm := ContextMemory{
Category: tag,
Chunk: sc.MemoryChunk,
AgeDays: ageDays,
}
if timeSensitiveTags[tag] && ageDays >= staleAfterDays {
cm.Stale = true
cm.StaleWarning = fmt.Sprintf(
"This %s memory is %d days old and may no longer reflect reality — verify it against the current code/state before acting on it.",
strings.TrimPrefix(tag, "#"), ageDays)
}
return cm
}
// FormatAsPrompt returns a plain-text block suitable for system prompt injection.
// Each memory is prefixed with its category, with an inline caution for stale
// time-sensitive entries. Use when MCP is unavailable (e.g. cron).
func (r ContextResult) FormatAsPrompt() string {
if len(r.Memories) == 0 {
return ""
}
var b strings.Builder
for _, m := range r.Memories {
b.WriteString(m.Category)
if m.Stale {
b.WriteString(fmt.Sprintf(" [⚠ %dd old — verify before trusting]", m.AgeDays))
}
b.WriteString(": ")
b.WriteString(strings.TrimSpace(m.Chunk.Text))
b.WriteString("\n\n")
}
return strings.TrimSuffix(b.String(), "\n\n")
}
// effectiveTime returns the most recent meaningful timestamp for a chunk:
// UpdatedAt if set, otherwise CreatedAt.
func effectiveTime(sc *storedChunk) time.Time {
if sc.UpdatedAt != nil {
return *sc.UpdatedAt
}
return sc.CreatedAt
}