Class: RubynCode::Memory::Store

Inherits:
Object
  • Object
show all
Defined in:
lib/rubyn_code/memory/store.rb

Overview

Writes and manages memories in SQLite, backed by an FTS5 full-text search index for fast retrieval. Handles expiration and relevance decay to keep the memory store manageable over time.

Constant Summary collapse

MEMORY_ATTR_MAP =
{
  content: ->(v) { v },
  tier: ->(v) { v },
  category: ->(v) { v },
  metadata: ->(v) { JSON.generate(v) },
  expires_at: ->(v) { v },
  relevance_score: lambda(&:to_f)
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(db, project_path:) ⇒ Store

Returns a new instance of Store.

Parameters:

  • db (DB::Connection)

    database connection

  • project_path (String)

    scoping path for this memory store



15
16
17
18
19
# File 'lib/rubyn_code/memory/store.rb', line 15

def initialize(db, project_path:)
  @db = db
  @project_path = project_path
  ensure_tables
end

Instance Method Details

#decay!(decay_rate: 0.01) ⇒ void

This method returns an undefined value.

Reduces the relevance_score of memories that have not been accessed recently, simulating natural memory decay.

Parameters:

  • decay_rate (Float) (defaults to: 0.01)

    amount to subtract from relevance_score (default 0.01)



105
106
107
108
109
110
111
112
113
114
# File 'lib/rubyn_code/memory/store.rb', line 105

def decay!(decay_rate: 0.01)
  cutoff = (Time.now.utc - 86_400).strftime('%Y-%m-%d %H:%M:%S') # 24 hours ago

  @db.execute(<<~SQL, [decay_rate, @project_path, cutoff])
    UPDATE memories
    SET relevance_score = MAX(0.0, relevance_score - ?)
    WHERE project_path = ?
      AND last_accessed_at < ?
  SQL
end

#delete(id) ⇒ void

This method returns an undefined value.

Deletes a memory and its FTS index entry.

Parameters:

  • id (String)


74
75
76
# File 'lib/rubyn_code/memory/store.rb', line 74

def delete(id)
  @db.execute('DELETE FROM memories WHERE id = ? AND project_path = ?', [id, @project_path])
end

#expire_old!Integer

Removes all memories whose expires_at is in the past.

Returns:

  • (Integer)

    number of expired memories deleted



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/rubyn_code/memory/store.rb', line 81

def expire_old!
  now = Time.now.utc.strftime('%Y-%m-%d %H:%M:%S')

  expired_ids = @db.query(
    'SELECT id FROM memories WHERE project_path = ? AND expires_at IS NOT NULL AND expires_at < ?',
    [@project_path, now]
  ).to_a.map { |row| row['id'] }

  return 0 if expired_ids.empty?

  placeholders = (['?'] * expired_ids.size).join(', ')
  @db.execute(
    "DELETE FROM memories WHERE id IN (#{placeholders}) AND project_path = ?",
    expired_ids + [@project_path]
  )

  expired_ids.size
end

#update(id, **attrs) ⇒ void

This method returns an undefined value.

Updates attributes on an existing memory.

Parameters:

  • id (String)

    the memory ID

  • attrs (Hash)

    attributes to update (content, tier, category, metadata, expires_at, relevance_score)



57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/rubyn_code/memory/store.rb', line 57

def update(id, **attrs)
  return if attrs.empty?

  sets, params = build_memory_update(attrs)
  return if sets.empty?

  params << id
  @db.execute(
    "UPDATE memories SET #{sets.join(', ')} WHERE id = ? AND project_path = '#{@project_path}'",
    params
  )
end

#write(content:, tier: 'medium', category: nil, metadata: {}, expires_at: nil) ⇒ MemoryRecord

Persists a new memory and updates the FTS index.

Parameters:

  • content (String)

    the memory content

  • tier (String) (defaults to: 'medium')

    retention tier (“short”, “medium”, “long”)

  • category (String, nil) (defaults to: nil)

    classification category

  • metadata (Hash) (defaults to: {})

    arbitrary metadata

  • expires_at (String, nil) (defaults to: nil)

    ISO 8601 expiration timestamp

Returns:



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/rubyn_code/memory/store.rb', line 29

def write(content:, tier: 'medium', category: nil, metadata: {}, expires_at: nil)
  validate_tier!(tier)
  validate_category!(category) if category

  id = SecureRandom.uuid
  now = Time.now.utc.strftime('%Y-%m-%d %H:%M:%S')
  meta_json = JSON.generate()

  @db.execute(<<~SQL, [id, @project_path, tier, category, content, 1.0, 0, now, expires_at, meta_json, now])
    INSERT INTO memories (id, project_path, tier, category, content,
                          relevance_score, access_count, last_accessed_at,
                          expires_at, metadata, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  SQL

  MemoryRecord.new(
    id: id, project_path: @project_path, tier: tier, category: category,
    content: content, relevance_score: 1.0, access_count: 0,
    last_accessed_at: now, expires_at: expires_at, metadata: ,
    created_at: now
  )
end