返回 DeepSeek-Reasonix
memory.go
根目录 / internal / taskmonitor / memory.go
1 package taskmonitor
2
3 import (
4 "context"
5 "fmt"
6 "sort"
7 "sync"
8 "time"
9 )
10
11 // defaultSchemaVersion is used when AppendEvent implicitly creates a
12 // TaskSnapshot for a task that has not been explicitly upserted.
13 const defaultSchemaVersion = 1
14
15 // InMemoryStore is a fully-in-memory Store implementation intended for
16 // testing and as a reference mock. It is goroutine-safe.
17 //
18 // IMPORTANT: This store grows without bound (no eviction, no capacity
19 // limits). It is NOT suitable for production use. Production stores
20 // must implement resource caps and persistence.
21 type InMemoryStore struct {
22 mu sync.RWMutex
23 tasks map[string]*TaskSnapshot // taskID → snapshot
24 events map[string][]TaskEvent // taskID → ordered events
25 byProj map[string]map[string]struct{} // projectDir → set of taskIDs
26 lastSeq map[string]int // taskID → last seen sequence
27 idemRecs map[string]*IdempotencyRecord // key → record
28 }
29
30 // NewInMemoryStore returns a ready-to-use InMemoryStore.
31 func NewInMemoryStore() *InMemoryStore {
32 return &InMemoryStore{
33 tasks: make(map[string]*TaskSnapshot),
34 events: make(map[string][]TaskEvent),
35 byProj: make(map[string]map[string]struct{}),
36 lastSeq: make(map[string]int),
37 }
38 }
39
40 // UpsertTask inserts or replaces a task snapshot and registers it under
41 // projectDir. It is a test/convenience helper — not part of the Store
42 // interface.
43 func (s *InMemoryStore) UpsertTask(projectDir string, snap TaskSnapshot) error {
44 if err := snap.Validate(); err != nil {
45 return fmt.Errorf("upsert task: %w", err)
46 }
47 s.mu.Lock()
48 defer s.mu.Unlock()
49 cp := snap
50 s.tasks[snap.TaskID] = &cp
51 if s.byProj[projectDir] == nil {
52 s.byProj[projectDir] = make(map[string]struct{})
53 }
54 s.byProj[projectDir][snap.TaskID] = struct{}{}
55 return nil
56 }
57
58 // AppendEvent appends a validated event to the task's event log.
59 // It is a test/convenience helper — not part of the Store interface.
60 //
61 // Validation rules:
62 // - Sequence must be strictly greater than the previous event's sequence
63 // (monotonic increasing, no duplicates allowed).
64 // - Events cannot be appended after the task has reached a terminal state.
65 // - If the task already exists (from UpsertTask), the event's TaskID and
66 // SessionID must match.
67 func (s *InMemoryStore) AppendEvent(projectDir string, ev TaskEvent) error {
68 if err := ev.Validate(); err != nil {
69 return fmt.Errorf("append event: %w", err)
70 }
71 s.mu.Lock()
72 defer s.mu.Unlock()
73
74 // --- sequence validation ---
75 prev, hasPrev := s.lastSeq[ev.TaskID]
76 if hasPrev {
77 if ev.Sequence <= prev {
78 return fmt.Errorf("append event: sequence %d is not strictly greater than previous %d",
79 ev.Sequence, prev)
80 }
81 }
82
83 // --- terminal-state guard ---
84 if snap, ok := s.tasks[ev.TaskID]; ok && snap.State.Terminal() {
85 return fmt.Errorf("append event: task %s is in terminal state %q",
86 ev.TaskID, snap.State)
87 }
88
89 // --- identity validation ---
90 if snap, ok := s.tasks[ev.TaskID]; ok {
91 if ev.SessionID != snap.SessionID {
92 return fmt.Errorf("append event: SessionID mismatch (event=%q, snapshot=%q)",
93 ev.SessionID, snap.SessionID)
94 }
95 }
96
97 // ensure task exists (at least minimally)
98 if _, ok := s.tasks[ev.TaskID]; !ok {
99 s.tasks[ev.TaskID] = &TaskSnapshot{
100 SchemaVersion: defaultSchemaVersion,
101 TaskID: ev.TaskID,
102 SessionID: ev.SessionID,
103 State: ev.State,
104 RuntimeState: ev.RuntimeState,
105 CreatedAt: ev.Timestamp,
106 UpdatedAt: ev.Timestamp,
107 }
108 }
109 s.lastSeq[ev.TaskID] = ev.Sequence
110
111 if s.byProj[projectDir] == nil {
112 s.byProj[projectDir] = make(map[string]struct{})
113 }
114 s.byProj[projectDir][ev.TaskID] = struct{}{}
115
116 // Update snapshot from event.
117 // ErrorCode and ErrorSummary are overwritten only when the event carries
118 // a non-empty value; they are NOT cleared by events that lack them.
119 snap := s.tasks[ev.TaskID]
120 snap.State = ev.State
121 if ev.RuntimeState != "" {
122 snap.RuntimeState = ev.RuntimeState
123 }
124 snap.UpdatedAt = ev.Timestamp
125 if ev.ErrorCode != "" {
126 snap.ErrorCode = ev.ErrorCode
127 }
128 if ev.ErrorSummary != "" {
129 snap.ErrorSummary = ev.ErrorSummary
130 }
131
132 s.events[ev.TaskID] = append(s.events[ev.TaskID], ev)
133 return nil
134 }
135
136 // ListTasks implements Store.
137 func (s *InMemoryStore) ListTasks(ctx context.Context, projectDir string) ([]TaskSnapshot, error) {
138 if err := ctx.Err(); err != nil {
139 return nil, err
140 }
141 s.mu.RLock()
142 defer s.mu.RUnlock()
143
144 var ids []string
145 if projectDir == "" {
146 for id := range s.tasks {
147 ids = append(ids, id)
148 }
149 } else {
150 proj, ok := s.byProj[projectDir]
151 if !ok {
152 return []TaskSnapshot{}, nil
153 }
154 for id := range proj {
155 ids = append(ids, id)
156 }
157 }
158
159 result := make([]TaskSnapshot, 0, len(ids))
160 for _, id := range ids {
161 snap, ok := s.tasks[id]
162 if !ok {
163 continue
164 }
165 cp := *snap
166 reconcileRuntime(&cp, timeNow())
167 result = append(result, cp)
168 }
169
170 sort.Slice(result, func(i, j int) bool {
171 return result[i].UpdatedAt.After(result[j].UpdatedAt)
172 })
173 return result, nil
174 }
175
176 // GetTask implements Store.
177 func (s *InMemoryStore) GetTask(ctx context.Context, projectDir string, taskID string) (*TaskSnapshot, error) {
178 if err := ctx.Err(); err != nil {
179 return nil, err
180 }
181 s.mu.RLock()
182 defer s.mu.RUnlock()
183
184 // When projectDir is specified, verify the task belongs to that project.
185 if projectDir != "" {
186 proj, ok := s.byProj[projectDir]
187 if !ok {
188 return nil, nil
189 }
190 if _, ok := proj[taskID]; !ok {
191 return nil, nil
192 }
193 }
194
195 snap, ok := s.tasks[taskID]
196 if !ok {
197 return nil, nil
198 }
199 cp := *snap
200 reconcileRuntime(&cp, timeNow())
201 return &cp, nil
202 }
203
204 // ListEvents implements Store.
205 func (s *InMemoryStore) ListEvents(ctx context.Context, projectDir string, taskID string, afterSequence int) ([]TaskEvent, error) {
206 if err := ctx.Err(); err != nil {
207 return nil, err
208 }
209 s.mu.RLock()
210 defer s.mu.RUnlock()
211
212 // When projectDir is specified, verify the task belongs to that project.
213 if projectDir != "" {
214 proj, ok := s.byProj[projectDir]
215 if !ok {
216 return []TaskEvent{}, nil
217 }
218 if _, ok := proj[taskID]; !ok {
219 return []TaskEvent{}, nil
220 }
221 }
222
223 all, ok := s.events[taskID]
224 if !ok {
225 return []TaskEvent{}, nil
226 }
227 result := make([]TaskEvent, 0)
228 for _, e := range all {
229 if e.Sequence > afterSequence {
230 result = append(result, e)
231 }
232 }
233 sort.Slice(result, func(i, j int) bool {
234 return result[i].Sequence < result[j].Sequence
235 })
236 return result, nil
237 }
238
239 // SaveTask implements WriteStore.
240 func (s *InMemoryStore) SaveTask(ctx context.Context, projectDir string, snap TaskSnapshot) error {
241 if err := ctx.Err(); err != nil {
242 return err
243 }
244 s.mu.Lock()
245 defer s.mu.Unlock()
246 existing, ok := s.tasks[snap.TaskID]
247 if !ok {
248 return fmt.Errorf("save task: task %s not found", snap.TaskID)
249 }
250 // Version must be strictly greater (CAS check)
251 if snap.Version <= existing.Version {
252 return fmt.Errorf("save task: %w: stored=%d, given=%d", ErrStoreVersionConflict, existing.Version, snap.Version)
253 }
254 cp := snap
255 s.tasks[snap.TaskID] = &cp
256 if s.byProj[projectDir] == nil {
257 s.byProj[projectDir] = make(map[string]struct{})
258 }
259 s.byProj[projectDir][snap.TaskID] = struct{}{}
260 return nil
261 }
262
263 // RenewRuntimeLease implements WriteStore.
264 func (s *InMemoryStore) RenewRuntimeLease(ctx context.Context, projectDir, taskID, ownerID string, leaseUntil time.Time) (bool, error) {
265 if err := ctx.Err(); err != nil {
266 return false, err
267 }
268 if ownerID == "" || leaseUntil.IsZero() {
269 return false, nil
270 }
271 s.mu.Lock()
272 defer s.mu.Unlock()
273 if projectDir != "" {
274 proj, ok := s.byProj[projectDir]
275 if !ok {
276 return false, nil
277 }
278 if _, ok := proj[taskID]; !ok {
279 return false, nil
280 }
281 }
282 snap, ok := s.tasks[taskID]
283 if !ok || snap.RuntimeOwnerID != ownerID || snap.State.Terminal() || snap.RuntimeState.Effective() != RuntimeStateAlive {
284 return false, nil
285 }
286 snap.Version++
287 snap.RuntimeLeaseUntil = leaseUntil
288 return true, nil
289 }
290
291 // AppendAuditEvent implements WriteStore.
292 func (s *InMemoryStore) AppendAuditEvent(ctx context.Context, projectDir string, ev TaskEvent) error {
293 s.mu.Lock()
294 defer s.mu.Unlock()
295 // Atomically assign next sequence
296 max := 0
297 for _, e := range s.events[ev.TaskID] {
298 if e.Sequence > max {
299 max = e.Sequence
300 }
301 }
302 ev.Sequence = max + 1
303 if err := ev.Validate(); err != nil {
304 return fmt.Errorf("append audit event: %w", err)
305 }
306 s.events[ev.TaskID] = append(s.events[ev.TaskID], ev)
307 return nil
308 }
309
310 // CheckIdempotency implements WriteStore.
311 func (s *InMemoryStore) CheckIdempotency(ctx context.Context, projectDir string, key string) (*IdempotencyRecord, error) {
312 s.mu.RLock()
313 defer s.mu.RUnlock()
314 _ = projectDir
315 _ = ctx
316 if s.idemRecs == nil {
317 return nil, nil
318 }
319 rec, ok := s.idemRecs[key]
320 if !ok {
321 return nil, nil
322 }
323 cp := *rec
324 return &cp, nil
325 }
326
327 // RecordIdempotency implements WriteStore.
328 func (s *InMemoryStore) RecordIdempotency(ctx context.Context, projectDir string, r IdempotencyRecord) error {
329 s.mu.Lock()
330 defer s.mu.Unlock()
331 _ = projectDir
332 _ = ctx
333 if s.idemRecs == nil {
334 s.idemRecs = make(map[string]*IdempotencyRecord)
335 }
336 // Reject if key already exists with different params
337 if existing, ok := s.idemRecs[r.Key]; ok {
338 if existing.Op != r.Op || existing.TaskID != r.TaskID || existing.Version != r.Version {
339 return fmt.Errorf("idempotency key conflict: different params")
340 }
341 return nil // already recorded, idempotent
342 }
343 cp := r
344 s.idemRecs[r.Key] = &cp
345 return nil
346 }
347
348 func (s *InMemoryStore) ClaimIdempotency(ctx context.Context, projectDir string, r IdempotencyRecord) (*IdempotencyRecord, error) {
349 s.mu.Lock()
350 defer s.mu.Unlock()
351 _ = projectDir
352 _ = ctx
353 if s.idemRecs == nil {
354 s.idemRecs = make(map[string]*IdempotencyRecord)
355 }
356 if r.ClaimedAt.IsZero() {
357 r.ClaimedAt = timeNow()
358 }
359 r.Pending = true
360 if existing, ok := s.idemRecs[r.Key]; ok {
361 cp := *existing
362 if cp.Pending && timeNow().Sub(cp.ClaimedAt) > 5*time.Minute {
363 cp = r
364 s.idemRecs[r.Key] = &cp
365 return nil, nil
366 }
367 return &cp, nil
368 }
369 cp := r
370 s.idemRecs[r.Key] = &cp
371 return nil, nil
372 }
373
374 func (s *InMemoryStore) FinalizeIdempotency(ctx context.Context, projectDir string, r IdempotencyRecord) error {
375 s.mu.Lock()
376 defer s.mu.Unlock()
377 _ = projectDir
378 _ = ctx
379 existing, ok := s.idemRecs[r.Key]
380 if !ok || existing.Op != r.Op || existing.TaskID != r.TaskID || existing.Version != r.Version {
381 return fmt.Errorf("idempotency key conflict: different params")
382 }
383 existing.Pending = false
384 return nil
385 }
386
387 func (s *InMemoryStore) ReleaseIdempotency(ctx context.Context, projectDir, key string) error {
388 s.mu.Lock()
389 defer s.mu.Unlock()
390 _ = projectDir
391 _ = ctx
392 if rec, ok := s.idemRecs[key]; ok && rec.Pending {
393 delete(s.idemRecs, key)
394 }
395 return nil
396 }
397
397 lines GO