Class: SwarmMemory::Core::Storage
- Inherits:
-
Object
- Object
- SwarmMemory::Core::Storage
- Defined in:
- lib/swarm_memory/core/storage.rb
Overview
High-level storage orchestration
Coordinates adapter operations, path normalization, embedding generation, and metadata extraction.
Instance Attribute Summary collapse
-
#adapter ⇒ Object
readonly
Returns the value of attribute adapter.
-
#semantic_index ⇒ SemanticIndex?
readonly
Get semantic index for semantic search operations.
Instance Method Summary collapse
-
#all_entries ⇒ Hash<String, Entry>
Get all entries (for optimization/analysis).
-
#clear ⇒ void
Clear all entries.
-
#delete(file_path:) ⇒ void
Delete an entry.
-
#glob(pattern:) ⇒ Array<Hash>
Search by glob pattern.
-
#grep(pattern:, case_insensitive: false, output_mode: "files_with_matches", path: nil) ⇒ Array<Hash>
Search by content pattern.
-
#initialize(adapter:, embedder: nil, semantic_weight: nil, keyword_weight: nil) ⇒ Storage
constructor
Initialize storage with an adapter.
-
#list(prefix: nil) ⇒ Array<Hash>
List all entries.
-
#read(file_path:) ⇒ String
Read content from storage, automatically following stub redirects.
-
#read_entry(file_path:, visited: []) ⇒ Entry
Read full entry with metadata, automatically following stub redirects.
-
#size ⇒ Integer
Get number of entries.
-
#total_size ⇒ Integer
Get total storage size.
-
#write(file_path:, content:, title:, metadata: nil, generate_embedding: nil) ⇒ Entry
Write content to storage.
Constructor Details
#initialize(adapter:, embedder: nil, semantic_weight: nil, keyword_weight: nil) ⇒ Storage
Initialize storage with an adapter
23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/swarm_memory/core/storage.rb', line 23 def initialize(adapter:, embedder: nil, semantic_weight: nil, keyword_weight: nil) raise ArgumentError, "adapter is required" unless adapter.is_a?(Adapters::Base) @adapter = adapter = # Create semantic index if embedder is provided @semantic_index = if = { adapter: adapter, embedder: } [:semantic_weight] = semantic_weight if semantic_weight [:keyword_weight] = keyword_weight if keyword_weight SemanticIndex.new(**) end end |
Instance Attribute Details
#adapter ⇒ Object (readonly)
Returns the value of attribute adapter.
15 16 17 |
# File 'lib/swarm_memory/core/storage.rb', line 15 def adapter @adapter end |
#semantic_index ⇒ SemanticIndex? (readonly)
Get semantic index for semantic search operations
41 42 43 |
# File 'lib/swarm_memory/core/storage.rb', line 41 def semantic_index @semantic_index end |
Instance Method Details
#all_entries ⇒ Hash<String, Entry>
Get all entries (for optimization/analysis)
246 247 248 |
# File 'lib/swarm_memory/core/storage.rb', line 246 def all_entries @adapter.all_entries end |
#clear ⇒ void
This method returns an undefined value.
Clear all entries
225 226 227 |
# File 'lib/swarm_memory/core/storage.rb', line 225 def clear @adapter.clear end |
#delete(file_path:) ⇒ void
This method returns an undefined value.
Delete an entry
185 186 187 188 |
# File 'lib/swarm_memory/core/storage.rb', line 185 def delete(file_path:) normalized_path = PathNormalizer.normalize(file_path) @adapter.delete(file_path: normalized_path) end |
#glob(pattern:) ⇒ Array<Hash>
Search by glob pattern
202 203 204 |
# File 'lib/swarm_memory/core/storage.rb', line 202 def glob(pattern:) @adapter.glob(pattern: pattern) end |
#grep(pattern:, case_insensitive: false, output_mode: "files_with_matches", path: nil) ⇒ Array<Hash>
Search by content pattern
213 214 215 216 217 218 219 220 |
# File 'lib/swarm_memory/core/storage.rb', line 213 def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches", path: nil) @adapter.grep( pattern: pattern, case_insensitive: case_insensitive, output_mode: output_mode, path: path, ) end |
#list(prefix: nil) ⇒ Array<Hash>
List all entries
194 195 196 |
# File 'lib/swarm_memory/core/storage.rb', line 194 def list(prefix: nil) @adapter.list(prefix: prefix) end |
#read(file_path:) ⇒ String
Read content from storage, automatically following stub redirects
107 108 109 110 |
# File 'lib/swarm_memory/core/storage.rb', line 107 def read(file_path:) entry = read_entry(file_path: file_path) entry.content end |
#read_entry(file_path:, visited: []) ⇒ Entry
Read full entry with metadata, automatically following stub redirects
Stub redirects are created by MemoryDefrag when merging/moving entries. This method transparently follows redirect chains up to 5 levels deep.
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/swarm_memory/core/storage.rb', line 121 def read_entry(file_path:, visited: []) normalized_path = PathNormalizer.normalize(file_path) # Detect circular redirects immediately if visited.include?(normalized_path) cycle = visited + [normalized_path] raise ArgumentError, "Circular redirect detected in memory storage: #{cycle.join(" → ")}\n\n" \ "This indicates corrupted stub files. Please run MemoryDefrag to repair:\n " \ "MemoryDefrag(action: \"analyze\")" end # Check depth limit (prevent infinite chains) if visited.size >= 5 chain = visited + [normalized_path] raise ArgumentError, "Memory redirect chain too deep (>5 redirects): #{chain.join(" → ")}\n\n" \ "This indicates fragmented memory storage. Please run maintenance:\n " \ "MemoryDefrag(action: \"full\", dry_run: true) # Preview first\n " \ "MemoryDefrag(action: \"full\", dry_run: false) # Execute" end # Read entry from adapter begin entry = @adapter.read_entry(file_path: normalized_path) rescue ArgumentError # If this is a redirect target that doesn't exist, provide helpful error if visited.empty? # Not a redirect, just re-raise original error raise else original_path = visited.first raise ArgumentError, "memory://#{original_path} was redirected to memory://#{normalized_path}, but the target was not found.\n\n" \ "The original entry may have been merged or moved incorrectly. " \ "Run MemoryDefrag to identify and fix broken redirects:\n " \ "MemoryDefrag(action: \"analyze\")" end end # Check if this is a stub redirect if entry. && entry.["stub"] == true redirect_target = entry.["redirect_to"] # Validate redirect target exists if redirect_target.nil? || redirect_target.strip.empty? raise ArgumentError, "memory://#{normalized_path} is a stub with invalid redirect metadata.\n\n" \ "This should never happen (stubs are created by MemoryDefrag). " \ "The stub file may be corrupted. Please report this as a bug." end # Follow redirect recursively, tracking visited paths return read_entry(file_path: redirect_target, visited: visited + [normalized_path]) end # Not a stub, return the entry entry end |
#size ⇒ Integer
Get number of entries
239 240 241 |
# File 'lib/swarm_memory/core/storage.rb', line 239 def size @adapter.size end |
#total_size ⇒ Integer
Get total storage size
232 233 234 |
# File 'lib/swarm_memory/core/storage.rb', line 232 def total_size @adapter.total_size end |
#write(file_path:, content:, title:, metadata: nil, generate_embedding: nil) ⇒ Entry
Write content to storage
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/swarm_memory/core/storage.rb', line 51 def write(file_path:, content:, title:, metadata: nil, generate_embedding: nil) # Normalize path normalized_path = PathNormalizer.normalize(file_path) # Generate embedding if requested and embedder available = nil = .nil? ? !.nil? : if && begin # Build searchable text for better semantic matching # Uses title + tags + first paragraph instead of full content searchable_text = build_searchable_text(content, title, ) # ALWAYS emit to LogStream (create if needed for debugging) # This ensures we can see what's being embedded begin if defined?(SwarmSDK::LogStream) SwarmSDK::LogStream.emit( type: "memory_embedding_generated", file_path: normalized_path, title: title, searchable_text_length: searchable_text.length, searchable_text_preview: searchable_text.slice(0, 300), full_searchable_text: searchable_text, metadata_tags: &.dig("tags"), metadata_domain: &.dig("domain"), ) end rescue StandardError => e # Don't fail if logging fails warn("Failed to log embedding: #{e.message}") end = .(searchable_text) rescue StandardError => e # Don't fail write if embedding generation fails warn("Warning: Failed to generate embedding for #{normalized_path}: #{e.message}") = nil end end # Write to adapter (metadata passed from tool parameters) @adapter.write( file_path: normalized_path, content: content, title: title, embedding: , metadata: , ) end |