返回 DeepSeek-Reasonix
memory.go
根目录 / internal / control / memory.go
1 package control
2
3 import (
4 "crypto/sha256"
5 "encoding/json"
6 "fmt"
7 "strings"
8 "sync"
9
10 "reasonix/internal/memory"
11 )
12
13 // memoryManager owns the session's loaded memory snapshot, the queue of pending
14 // turn-tail notes, and the serialization of memory writes — behind its own locks
15 // and off the controller's c.mu. Like goalMachine it is a strict leaf: its
16 // methods only touch its own state and never call back into the Controller, so a
17 // memory-panel save can't stall an approval or status poll on c.mu.
18 //
19 // set is an immutable snapshot: reads take mu briefly and return the pointer.
20 // Writes are serialized by writeMu and do their disk I/O (the doc/store write
21 // plus the memory.Load re-discovery) OFF mu, taking mu only to swap the freshly
22 // discovered snapshot in and queue the turn-tail note — so a write never holds a
23 // lock across a filesystem walk. A turn-tail note is queued for each write so the
24 // change applies this session without disturbing the cache-stable system prefix
25 // (it folds into the prefix on the next session). All write methods are no-ops
26 // returning "" when memory is disabled (set == nil).
27 type memoryManager struct {
28 // mu guards set (the snapshot pointer) and pending (the turn-tail queue);
29 // every critical section under it is short and non-blocking.
30 mu sync.Mutex
31 set *memory.Set
32 // pending holds memory notes added mid-session (via "#" quick-add or a memory
33 // edit) that haven't yet been folded into a turn. Compose drains it onto the
34 // next outgoing turn — never into the cache-stable system prefix — so a fresh
35 // memory takes effect this session without busting the prompt cache; it joins
36 // the prefix naturally on the next session.
37 pending []string
38 lastRecall memory.RecallResult
39 autoWrites map[[32]byte]int
40
41 // writeMu serializes memory writes so each write+reload+swap is atomic with
42 // respect to the others. Taken OFF mu, so a read (current/drainPending) never
43 // blocks behind a write's disk I/O.
44 writeMu sync.Mutex
45 }
46
47 func (m *memoryManager) authorizeAutoRemember(args json.RawMessage) {
48 key := sha256.Sum256(args)
49 m.mu.Lock()
50 if m.autoWrites == nil {
51 m.autoWrites = map[[32]byte]int{}
52 }
53 m.autoWrites[key]++
54 m.mu.Unlock()
55 }
56
57 func (m *memoryManager) revokeAutoRemember(args json.RawMessage) {
58 key := sha256.Sum256(args)
59 m.mu.Lock()
60 delete(m.autoWrites, key)
61 m.mu.Unlock()
62 }
63
64 func (m *memoryManager) clearAutoRemember() {
65 m.mu.Lock()
66 m.autoWrites = nil
67 m.mu.Unlock()
68 }
69
70 func (m *memoryManager) claimAutoRemember(args json.RawMessage) bool {
71 key := sha256.Sum256(args)
72 m.mu.Lock()
73 defer m.mu.Unlock()
74 if m.autoWrites[key] <= 0 {
75 return false
76 }
77 if m.autoWrites[key] == 1 {
78 delete(m.autoWrites, key)
79 } else {
80 m.autoWrites[key]--
81 }
82 return true
83 }
84
85 func (m *memoryManager) recall(query string) memory.RecallResult {
86 mem := m.current()
87 store := memory.Store{}
88 if mem != nil {
89 store = mem.Store
90 }
91 result := memory.AutoRecall(store, query, memory.RecallOptions{})
92 m.recordRecall(result)
93 return result
94 }
95
96 func (m *memoryManager) recordRecall(result memory.RecallResult) {
97 m.mu.Lock()
98 m.lastRecall = result
99 m.mu.Unlock()
100 }
101
102 func (m *memoryManager) lastRecallResult() memory.RecallResult {
103 m.mu.Lock()
104 defer m.mu.Unlock()
105 return m.lastRecall
106 }
107
108 func newMemoryManager(set *memory.Set) memoryManager {
109 return memoryManager{set: set}
110 }
111
112 // current returns the loaded snapshot (nil when memory is disabled). The returned
113 // *Set is immutable — mutations go through quickAdd / saveDoc / saveMemory.
114 func (m *memoryManager) current() *memory.Set {
115 m.mu.Lock()
116 defer m.mu.Unlock()
117 return m.set
118 }
119
120 // drainPending returns and clears the queued turn-tail notes, for Compose to fold
121 // onto the next outgoing turn.
122 func (m *memoryManager) drainPending() []string {
123 m.mu.Lock()
124 defer m.mu.Unlock()
125 notes := m.pending
126 m.pending = nil
127 return notes
128 }
129
130 // applyWrite re-discovers memory from disk (off-lock, the expensive part) then,
131 // under a brief mu, swaps the fresh snapshot in and queues the turn-tail note so a
132 // later current() reflects the just-applied write. mem is the snapshot taken at
133 // the start of the writeMu-serialized write and supplies the discovery roots.
134 // Callers hold writeMu.
135 func (m *memoryManager) applyWrite(mem *memory.Set, note string) {
136 reloaded := memory.Load(memory.Options{CWD: mem.CWD, UserDir: mem.UserDir})
137 m.mu.Lock()
138 if note != "" {
139 m.pending = append(m.pending, note)
140 }
141 m.set = reloaded
142 m.mu.Unlock()
143 }
144
145 // quickAdd appends a one-line note to the doc-memory file for scope (project
146 // REASONIX.md by default) — the write side of "#<note>". Returns the file written.
147 func (m *memoryManager) quickAdd(scope memory.Scope, note string) (string, error) {
148 m.writeMu.Lock()
149 defer m.writeMu.Unlock()
150 mem := m.current()
151 if mem == nil {
152 return "", nil
153 }
154 path := mem.DocPath(scope)
155 if path == "" {
156 return "", fmt.Errorf("no target file for memory scope %q", scope)
157 }
158 if err := memory.AppendDoc(path, note); err != nil {
159 return "", err
160 }
161 m.applyWrite(mem, note)
162 return path, nil
163 }
164
165 // saveDoc overwrites a recognized memory doc with body — the save side of the
166 // desktop panel's in-place editor. Returns the file written.
167 func (m *memoryManager) saveDoc(path, body string) (string, error) {
168 m.writeMu.Lock()
169 defer m.writeMu.Unlock()
170 mem := m.current()
171 if mem == nil {
172 return "", nil
173 }
174 written, err := mem.WriteDoc(path, body)
175 if err != nil {
176 return "", err
177 }
178 // Inject the new content once on the next turn: the cached prefix still holds
179 // the pre-edit version this session, so handing the model the current text
180 // avoids a stale-guidance gap until the next session re-folds it into the
181 // prefix. Trimmed to a single tail note (drained by Compose), not per-turn.
182 m.applyWrite(mem,
183 "Memory file "+written+" was just edited. Its current contents:\n"+strings.TrimSpace(body))
184 return written, nil
185 }
186
187 // saveMemory writes an active auto-memory fact and refreshes the in-session
188 // snapshot. It is the explicit user-confirmed counterpart to the model-owned
189 // remember tool, used by management surfaces that preview a candidate first.
190 func (m *memoryManager) saveMemory(fact memory.Memory) (string, error) {
191 m.writeMu.Lock()
192 defer m.writeMu.Unlock()
193 mem := m.current()
194 if mem == nil {
195 return "", nil
196 }
197 path, err := mem.Store.Save(fact)
198 if err != nil {
199 return "", err
200 }
201 m.applyWrite(mem,
202 "Saved memory \""+fact.Name+"\": "+strings.Join(strings.Fields(fact.Description), " ")+"\n"+strings.TrimSpace(fact.Body))
203 return path, nil
204 }
205
206 // forget removes a saved auto-memory by name — the panel/TUI forget action, the
207 // manual counterpart to the model's `forget` tool. It queues a turn-tail note so
208 // the removal applies this session (the cached prefix still lists the fact until
209 // the next session re-folds the index). The file is archived for traceability by
210 // Store.Delete.
211 func (m *memoryManager) forget(name string) error {
212 m.writeMu.Lock()
213 defer m.writeMu.Unlock()
214 mem := m.current()
215 if mem == nil {
216 return nil
217 }
218 if err := mem.Store.Delete(name); err != nil {
219 return err
220 }
221 m.applyWrite(mem,
222 "Forgot memory \""+name+"\" — disregard its loaded guidance and background-index entry for the rest of this session.")
223 return nil
224 }
225
226 func (m *memoryManager) revisions(ref string) []memory.Memory {
227 mem := m.current()
228 if mem == nil {
229 return nil
230 }
231 return mem.Store.Revisions(ref)
232 }
233
234 func (m *memoryManager) restore(ref string, revision int) (memory.Memory, error) {
235 m.writeMu.Lock()
236 defer m.writeMu.Unlock()
237 mem := m.current()
238 if mem == nil {
239 return memory.Memory{}, fmt.Errorf("memory unavailable")
240 }
241 result, err := mem.Store.Restore(ref, revision)
242 if err != nil {
243 return memory.Memory{}, err
244 }
245 m.applyWrite(mem, fmt.Sprintf("Restored memory %q as revision %d: %s\n%s",
246 result.Memory.Name, result.Memory.Revision, strings.Join(strings.Fields(result.Memory.Description), " "), strings.TrimSpace(result.Memory.Body)))
247 return result.Memory, nil
248 }
249
250 func (m *memoryManager) restoreArchived(archivePath string) (memory.Memory, error) {
251 m.writeMu.Lock()
252 defer m.writeMu.Unlock()
253 mem := m.current()
254 if mem == nil {
255 return memory.Memory{}, fmt.Errorf("memory unavailable")
256 }
257 result, err := mem.Store.RestoreArchived(archivePath)
258 if err != nil {
259 return memory.Memory{}, err
260 }
261 m.applyWrite(mem, fmt.Sprintf("Recovered archived memory %q as revision %d: %s\n%s",
262 result.Memory.Name, result.Memory.Revision, strings.Join(strings.Fields(result.Memory.Description), " "), strings.TrimSpace(result.Memory.Body)))
263 return result.Memory, nil
264 }
265
266 // queue rides a note on the next turn — the model's remember/forget tool path
267 // (memory.Queue). It refreshes the snapshot a memory panel reads when memory is
268 // enabled, and still queues the turn-tail note when it isn't (there's no snapshot
269 // to re-discover).
270 func (m *memoryManager) queue(note string) {
271 m.writeMu.Lock()
272 defer m.writeMu.Unlock()
273 if mem := m.current(); mem != nil {
274 m.applyWrite(mem, note)
275 return
276 }
277 m.mu.Lock()
278 m.pending = append(m.pending, note)
279 m.mu.Unlock()
280 }
281
281 lines GO