package memory import ( "database/sql" "fmt" "os" "path/filepath" "sync" "time" _ "modernc.org/sqlite" ) type MemoryType string const ( TypePreference MemoryType = "preference" TypeContext MemoryType = "context" TypeSummary MemoryType = "summary" TypeFact MemoryType = "fact" TypePattern MemoryType = "pattern" ) type Memory struct { ID string `json:"id"` Type MemoryType `json:"type"` Key string `json:"key"` Content string `json:"content"` Tags string `json:"tags,omitempty"` Source string `json:"source,omitempty"` Confidence float64 `json:"confidence,omitempty"` AccessCount int `json:"access_count"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type Store struct { db *sql.DB path string mu sync.RWMutex } func NewStore() (*Store, error) { dbPath, err := dbPath() if err != nil { return nil, fmt.Errorf("get db path: %w", err) } if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil { return nil, fmt.Errorf("create memory dir: %w", err) } db, err := sql.Open("sqlite", dbPath) if err != nil { return nil, fmt.Errorf("open memory db: %w", err) } db.SetMaxOpenConns(1) s := &Store{db: db, path: dbPath} if err := s.migrate(); err != nil { db.Close() return nil, fmt.Errorf("migrate: %w", err) } return s, nil } func (s *Store) Close() error { return s.db.Close() } func (s *Store) Store(m *Memory) error { s.mu.Lock() defer s.mu.Unlock() if m.ID == "" { m.ID = generateID() } now := time.Now() if m.CreatedAt.IsZero() { m.CreatedAt = now } m.UpdatedAt = now _, err := s.db.Exec(` INSERT INTO memories (id, type, key, content, tags, source, confidence, access_count, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET type = excluded.type, key = excluded.key, content = excluded.content, tags = excluded.tags, source = excluded.source, confidence = excluded.confidence, access_count = excluded.access_count, updated_at = excluded.updated_at `, m.ID, string(m.Type), m.Key, m.Content, m.Tags, m.Source, m.Confidence, m.AccessCount, m.CreatedAt, m.UpdatedAt) return err } func (s *Store) Get(id string) (*Memory, error) { s.mu.RLock() defer s.mu.RUnlock() m := &Memory{} err := s.db.QueryRow(` SELECT id, type, key, content, tags, source, confidence, access_count, created_at, updated_at FROM memories WHERE id = ? `, id).Scan(&m.ID, &m.Type, &m.Key, &m.Content, &m.Tags, &m.Source, &m.Confidence, &m.AccessCount, &m.CreatedAt, &m.UpdatedAt) if err == nil { s.incrementAccess(id) } return m, err } func (s *Store) Delete(id string) error { s.mu.Lock() defer s.mu.Unlock() _, err := s.db.Exec(`DELETE FROM memories WHERE id = ?`, id) return err } func (s *Store) List(memType MemoryType, limit, offset int) ([]Memory, error) { s.mu.RLock() defer s.mu.RUnlock() if limit <= 0 { limit = 50 } if limit > 200 { limit = 200 } var rows *sql.Rows var err error if memType != "" { rows, err = s.db.Query(` SELECT id, type, key, content, tags, source, confidence, access_count, created_at, updated_at FROM memories WHERE type = ? ORDER BY updated_at DESC LIMIT ? OFFSET ? `, string(memType), limit, offset) } else { rows, err = s.db.Query(` SELECT id, type, key, content, tags, source, confidence, access_count, created_at, updated_at FROM memories ORDER BY updated_at DESC LIMIT ? OFFSET ? `, limit, offset) } if err != nil { return nil, err } defer rows.Close() return scanMemories(rows) } func (s *Store) Count() (int, error) { s.mu.RLock() defer s.mu.RUnlock() var count int err := s.db.QueryRow(`SELECT COUNT(*) FROM memories`).Scan(&count) return count, err } func (s *Store) incrementAccess(id string) { go func() { s.db.Exec(`UPDATE memories SET access_count = access_count + 1 WHERE id = ?`, id) }() } func (s *Store) migrate() error { _, err := s.db.Exec(` CREATE TABLE IF NOT EXISTS memories ( id TEXT PRIMARY KEY, type TEXT NOT NULL, key TEXT NOT NULL, content TEXT NOT NULL, tags TEXT DEFAULT '', source TEXT DEFAULT '', confidence REAL DEFAULT 0.5, access_count INTEGER DEFAULT 0, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL ) `) if err != nil { return err } _, err = s.db.Exec(` CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type) `) if err != nil { return err } _, err = s.db.Exec(` CREATE INDEX IF NOT EXISTS idx_memories_key ON memories(key) `) if err != nil { return err } _, err = s.db.Exec(` CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5( key, content, tags, content=memories, content_rowid=rowid ) `) if err != nil { return err } _, err = s.db.Exec(` CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN INSERT INTO memories_fts(rowid, key, content, tags) VALUES (new.rowid, new.key, new.content, new.tags); END `) if err != nil { return err } _, err = s.db.Exec(` CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN INSERT INTO memories_fts(memories_fts, rowid, key, content, tags) VALUES ('delete', old.rowid, old.key, old.content, old.tags); END `) if err != nil { return err } _, err = s.db.Exec(` CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN INSERT INTO memories_fts(memories_fts, rowid, key, content, tags) VALUES ('delete', old.rowid, old.key, old.content, old.tags); INSERT INTO memories_fts(rowid, key, content, tags) VALUES (new.rowid, new.key, new.content, new.tags); END `) return err } func scanMemories(rows *sql.Rows) ([]Memory, error) { var memories []Memory for rows.Next() { var m Memory err := rows.Scan(&m.ID, &m.Type, &m.Key, &m.Content, &m.Tags, &m.Source, &m.Confidence, &m.AccessCount, &m.CreatedAt, &m.UpdatedAt) if err != nil { return memories, err } memories = append(memories, m) } return memories, nil } func dbPath() (string, error) { home, err := os.UserHomeDir() if err != nil { return "", err } return filepath.Join(home, ".muyue", "memory", "memories.db"), nil } func generateID() string { return fmt.Sprintf("mem_%d", time.Now().UnixNano()) }