This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/store/sqlite/memory.go
2026-03-12 22:49:35 +00:00

257 lines
6.9 KiB
Go

package sqlite
import (
"context"
"database/sql"
"fmt"
"strings"
"git.gobha.me/xcaliber/chat-switchboard/models"
"git.gobha.me/xcaliber/chat-switchboard/store"
)
type MemoryStore struct{}
func NewMemoryStore() *MemoryStore { return &MemoryStore{} }
func (s *MemoryStore) Upsert(ctx context.Context, m *models.Memory) error {
if m.ID == "" {
m.ID = store.NewID()
}
if m.Status == "" {
m.Status = models.MemoryStatusActive
}
if m.Confidence == 0 {
m.Confidence = 1.0
}
_, err := DB.ExecContext(ctx, `
INSERT INTO memories (id, scope, owner_id, user_id, key, value,
source_channel_id, confidence, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (scope, owner_id, COALESCE(user_id, '00000000-0000-0000-0000-000000000000'), key)
DO UPDATE SET
value = excluded.value,
confidence = excluded.confidence,
status = excluded.status,
source_channel_id = excluded.source_channel_id,
updated_at = datetime('now')
`, m.ID, m.Scope, m.OwnerID, m.UserID, m.Key, m.Value,
m.SourceChannelID, m.Confidence, m.Status)
return err
}
func (s *MemoryStore) Update(ctx context.Context, m *models.Memory) error {
_, err := DB.ExecContext(ctx, `
UPDATE memories
SET key = ?, value = ?, confidence = ?, status = ?,
source_channel_id = ?, updated_at = datetime('now')
WHERE id = ?
`, m.Key, m.Value, m.Confidence, m.Status, m.SourceChannelID, m.ID)
return err
}
func (s *MemoryStore) GetByID(ctx context.Context, id string) (*models.Memory, error) {
m := &models.Memory{}
err := DB.QueryRowContext(ctx, `
SELECT id, scope, owner_id, user_id, key, value,
source_channel_id, confidence, status, created_at, updated_at
FROM memories WHERE id = ?
`, id).Scan(
&m.ID, &m.Scope, &m.OwnerID, &m.UserID, &m.Key, &m.Value,
&m.SourceChannelID, &m.Confidence, &m.Status,
st(&m.CreatedAt), st(&m.UpdatedAt),
)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("memory not found: %s", id)
}
return m, err
}
func (s *MemoryStore) List(ctx context.Context, f models.MemoryFilter) ([]models.Memory, error) {
clauses := []string{"scope = ?", "owner_id = ?"}
args := []interface{}{f.Scope, f.OwnerID}
if f.UserID != nil {
clauses = append(clauses, "user_id = ?")
args = append(args, *f.UserID)
}
status := f.Status
if status == "" {
status = models.MemoryStatusActive
}
clauses = append(clauses, "status = ?")
args = append(args, status)
if f.Query != "" {
// SQLite: LIKE-based keyword search (no tsvector)
clauses = append(clauses, "(key || ' ' || value) LIKE ?")
args = append(args, "%"+f.Query+"%")
}
limit := f.Limit
if limit <= 0 || limit > 200 {
limit = 50
}
args = append(args, limit)
query := fmt.Sprintf(`
SELECT id, scope, owner_id, user_id, key, value,
source_channel_id, confidence, status, created_at, updated_at
FROM memories
WHERE %s
ORDER BY confidence DESC, updated_at DESC
LIMIT ?
`, strings.Join(clauses, " AND "))
rows, err := DB.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
return s.scanMemories(rows)
}
func (s *MemoryStore) Delete(ctx context.Context, id string) error {
_, err := DB.ExecContext(ctx, `DELETE FROM memories WHERE id = ?`, id)
return err
}
func (s *MemoryStore) Archive(ctx context.Context, id string) error {
_, err := DB.ExecContext(ctx, `
UPDATE memories SET status = 'archived', updated_at = datetime('now') WHERE id = ?
`, id)
return err
}
func (s *MemoryStore) Recall(ctx context.Context, userID string, personaID *string, query string, limit int) ([]models.Memory, error) {
if limit <= 0 || limit > 100 {
limit = 20
}
// Build UNION query across applicable scopes
parts := []string{}
args := []interface{}{}
// 1. User-scope memories
userPart := `
SELECT id, scope, owner_id, user_id, key, value,
source_channel_id, confidence, status, created_at, updated_at
FROM memories
WHERE scope = 'user' AND owner_id = ? AND status = 'active'
`
args = append(args, userID)
if query != "" {
userPart += " AND (key || ' ' || value) LIKE ?"
args = append(args, "%"+query+"%")
}
parts = append(parts, userPart)
// 2. Persona-scope memories
if personaID != nil && *personaID != "" {
personaPart := `
SELECT id, scope, owner_id, user_id, key, value,
source_channel_id, confidence, status, created_at, updated_at
FROM memories
WHERE scope = 'persona' AND owner_id = ? AND status = 'active'
`
args = append(args, *personaID)
if query != "" {
personaPart += " AND (key || ' ' || value) LIKE ?"
args = append(args, "%"+query+"%")
}
parts = append(parts, personaPart)
// 3. Persona+user memories
puPart := `
SELECT id, scope, owner_id, user_id, key, value,
source_channel_id, confidence, status, created_at, updated_at
FROM memories
WHERE scope = 'persona_user' AND owner_id = ? AND user_id = ? AND status = 'active'
`
args = append(args, *personaID, userID)
if query != "" {
puPart += " AND (key || ' ' || value) LIKE ?"
args = append(args, "%"+query+"%")
}
parts = append(parts, puPart)
}
fullQuery := fmt.Sprintf(`
SELECT * FROM (%s)
ORDER BY
CASE scope
WHEN 'persona_user' THEN 0
WHEN 'persona' THEN 1
WHEN 'user' THEN 2
END,
confidence DESC,
updated_at DESC
LIMIT ?
`, strings.Join(parts, " UNION ALL "))
args = append(args, limit)
rows, err := DB.QueryContext(ctx, fullQuery, args...)
if err != nil {
return nil, err
}
defer rows.Close()
return s.scanMemories(rows)
}
func (s *MemoryStore) CountByOwner(ctx context.Context, scope, ownerID string) (int, error) {
var count int
err := DB.QueryRowContext(ctx, `
SELECT COUNT(*) FROM memories
WHERE scope = ? AND owner_id = ? AND status = 'active'
`, scope, ownerID).Scan(&count)
return count, err
}
func (s *MemoryStore) ListByStatus(ctx context.Context, status string, limit int) ([]models.Memory, error) {
if limit <= 0 || limit > 500 {
limit = 100
}
rows, err := DB.QueryContext(ctx, `
SELECT id, scope, owner_id, user_id, key, value,
source_channel_id, confidence, status, created_at, updated_at
FROM memories
WHERE status = ?
ORDER BY created_at DESC
LIMIT ?
`, status, limit)
if err != nil {
return nil, err
}
defer rows.Close()
return s.scanMemories(rows)
}
// ── Helpers ─────────────────────────────────
func (s *MemoryStore) scanMemories(rows *sql.Rows) ([]models.Memory, error) {
var memories []models.Memory
for rows.Next() {
var m models.Memory
if err := rows.Scan(
&m.ID, &m.Scope, &m.OwnerID, &m.UserID, &m.Key, &m.Value,
&m.SourceChannelID, &m.Confidence, &m.Status,
st(&m.CreatedAt), st(&m.UpdatedAt),
); err != nil {
return nil, err
}
memories = append(memories, m)
}
if memories == nil {
memories = []models.Memory{}
}
return memories, rows.Err()
}
// ensure compile-time interface satisfaction
var _ store.MemoryStore = (*MemoryStore)(nil)