Class: Rubino::Memory::Store
- Inherits:
-
Object
- Object
- Rubino::Memory::Store
- Defined in:
- lib/rubino/memory/store.rb
Overview
Primary storage interface for persistent memories. Handles CRUD operations on the memories table.
Defined Under Namespace
Classes: BudgetExceededError, ThreatDetectedError
Constant Summary collapse
- VALID_KINDS =
%w[ user_profile preference project_context technical_decision fact task_state tool_result ].freeze
Class Method Summary collapse
-
.group_for_kind(kind) ⇒ Object
Returns the budget group a kind belongs to: - “user” → user_profile (its own dedicated budget) - “memory” → everything else (shared general-memory budget).
Instance Method Summary collapse
-
#by_kind(kind, limit: 50) ⇒ Object
Returns memories of a specific kind.
-
#count ⇒ Object
Returns the total count of stored memories.
-
#create(kind:, content:, source_session_id: nil, confidence: 1.0, metadata: {}) ⇒ Object
Creates a new memory entry.
-
#delete(id) ⇒ Object
Deletes a memory.
-
#find(id) ⇒ Object
Finds a memory by ID (supports prefix matching).
-
#initialize(db: nil, config: nil) ⇒ Store
constructor
A new instance of Store.
-
#list(kind: nil, limit: 20) ⇒ Object
Lists memories with optional filters.
-
#total_chars_for_group(group) ⇒ Object
Sum of content length across every row in the given group.
-
#update(id, content:, confidence: nil) ⇒ Object
Updates a memory’s content.
-
#within_limit(char_limit:) ⇒ Object
Returns all memories within the character limit.
Constructor Details
Class Method Details
.group_for_kind(kind) ⇒ Object
Returns the budget group a kind belongs to:
-
“user” → user_profile (its own dedicated budget)
-
“memory” → everything else (shared general-memory budget)
161 162 163 |
# File 'lib/rubino/memory/store.rb', line 161 def self.group_for_kind(kind) kind == "user_profile" ? "user" : "memory" end |
Instance Method Details
#by_kind(kind, limit: 50) ⇒ Object
Returns memories of a specific kind
126 127 128 129 130 131 132 |
# File 'lib/rubino/memory/store.rb', line 126 def by_kind(kind, limit: 50) @db[:memories] .where(kind: kind) .order(Sequel.desc(:confidence), Sequel.desc(:created_at)) .limit(limit) .all end |
#count ⇒ Object
Returns the total count of stored memories
154 155 156 |
# File 'lib/rubino/memory/store.rb', line 154 def count @db[:memories].count end |
#create(kind:, content:, source_session_id: nil, confidence: 1.0, metadata: {}) ⇒ Object
Creates a new memory entry.
Two boundary checks run before the row is inserted:
1. ThreatScanner — refuses prompt-injection, exfil, invisible
unicode, etc. Memory persists across sessions, so a tainted
write would keep biasing every future prompt.
2. char-budget — refuses writes that would push the group's
total past memory_char_limit / memory_user_char_limit. Lets
callers (Tools::MemoryTool) surface a "delete or replace
older entries first" message instead of silently truncating
at read-time.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/rubino/memory/store.rb', line 66 def create(kind:, content:, source_session_id: nil, confidence: 1.0, metadata: {}) validate_kind!(kind) enforce_threat_scan!(content) enforce_char_budget!(kind, content) now = Time.now.utc.iso8601 id = SecureRandom.uuid @db[:memories].insert( id: id, kind: kind, content: content, source_session_id: source_session_id, confidence: confidence, metadata_json: .empty? ? nil : JSON.generate(), created_at: now, updated_at: now ) find(id) end |
#delete(id) ⇒ Object
Deletes a memory
120 121 122 123 |
# File 'lib/rubino/memory/store.rb', line 120 def delete(id) count = @db[:memories].where(Sequel.like(:id, "#{id}%")).delete count > 0 end |
#find(id) ⇒ Object
Finds a memory by ID (supports prefix matching)
89 90 91 |
# File 'lib/rubino/memory/store.rb', line 89 def find(id) @db[:memories].where(Sequel.like(:id, "#{id}%")).first end |
#list(kind: nil, limit: 20) ⇒ Object
Lists memories with optional filters
94 95 96 97 98 |
# File 'lib/rubino/memory/store.rb', line 94 def list(kind: nil, limit: 20) dataset = @db[:memories].order(Sequel.desc(:created_at)).limit(limit) dataset = dataset.where(kind: kind) if kind dataset.all end |
#total_chars_for_group(group) ⇒ Object
Sum of content length across every row in the given group.
166 167 168 169 170 171 172 |
# File 'lib/rubino/memory/store.rb', line 166 def total_chars_for_group(group) if group == "user" @db[:memories].where(kind: "user_profile").sum(Sequel.function(:length, :content)).to_i else @db[:memories].exclude(kind: "user_profile").sum(Sequel.function(:length, :content)).to_i end end |
#update(id, content:, confidence: nil) ⇒ Object
Updates a memory’s content.
Same two boundary checks as create — the replace path was a hole that let an agent rewrite a benign entry with prompt-injection / exfil content without going through ThreatScanner, and let a chain of replaces grow a group past its char budget one byte at a time. The budget check subtracts the old row’s length before re-adding the new, otherwise a same-size edit would be flagged as over budget when it isn’t.
109 110 111 112 113 114 115 116 117 |
# File 'lib/rubino/memory/store.rb', line 109 def update(id, content:, confidence: nil) existing = find(id) enforce_threat_scan!(content) enforce_char_budget_for_update!(existing, content) if existing attrs = { content: content, updated_at: Time.now.utc.iso8601 } attrs[:confidence] = confidence if confidence @db[:memories].where(id: id).update(attrs) end |
#within_limit(char_limit:) ⇒ Object
Returns all memories within the character limit
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/rubino/memory/store.rb', line 135 def within_limit(char_limit:) memories = @db[:memories] .order(Sequel.desc(:confidence), Sequel.desc(:updated_at)) .all selected = [] total_chars = 0 memories.each do |m| break if total_chars + m[:content].length > char_limit selected << m total_chars += m[:content].length end selected end |