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