lex-agentic-memory 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e7d4306cac20ea6106de6c5961c23651a3a891d1891ae986869904141e8374c
4
- data.tar.gz: 77a5a10d16f09dd6c9f3057219435c676909529740e2d6d429932f542d601e46
3
+ metadata.gz: 44a4999991835bdd28a7189717f6a53f38928644a86c7f653517331687659901
4
+ data.tar.gz: b3aeb50fddc4b82580430829ec924549690b8dd6171be74ff9213db40b135888
5
5
  SHA512:
6
- metadata.gz: d1d4ad0cb84988d8d76965f45017429caa2d3e77d6bf184673d4353fc249547d61da10853c297891c496c56862d93fa010130f53c28fbd054b61cff91bfb6c0f
7
- data.tar.gz: 86ff6e678ebc1d9f3690966cbef2a3e6f90fb1635eaa78b0911734cf2a3dd230b63672365c9c1e87cf4a3081513cd05d4e2cf74258b969978f9d8512a4347bc2
6
+ metadata.gz: 32930b8b8092925f0793eef8ae8b1a6764d50cbc9692bb2952c46fb969137b41609eed96ecb455fdc7d0518373ebd912075eff8ac47c4deb8e97e1fc9176962b
7
+ data.tar.gz: a142cbf70545c4f80717ddda3d81abab26a1cc2cf5d8e40584625a416629d1551991d716fc6a9592fff194b523b0ebed2963be24fa414039cf11bdb3c934e60a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.11] - 2026-03-25
4
+
5
+ ### Added
6
+ - `Helpers::HotTier` module: Redis hot-tier cache in front of PostgresStore using `Legion::Cache::RedisHash`. Stores traces as Redis hashes with 24-hour TTL, maintains a sorted-set index per tenant, and provides `cache_trace`, `fetch_trace`, and `evict_trace` operations
7
+ - `PostgresStore#retrieve` checks hot tier first; falls through to DB on miss and populates hot tier on DB hit
8
+ - `PostgresStore#store` writes through to hot tier after successful DB write
9
+ - `PostgresStore#delete` evicts from hot tier before DB delete
10
+ - `PostgresStore#update` evicts stale hot-tier entry after DB update
11
+ - 32 new specs covering HotTier interface, serialize/deserialize round-trip, availability guard, and all four PostgresStore integration points
12
+
13
+ ## [0.1.10] - 2026-03-25
14
+
15
+ ### Added
16
+ - `Helpers::PostgresStore`: write-through durable store backed by Legion::Data (PostgreSQL or MySQL), scoped by tenant_id. Implements full store interface: store, retrieve, retrieve_by_type, retrieve_by_domain, all_traces, delete, update, record_coactivation, associations_for, walk_associations, delete_lowest_confidence, delete_least_recently_used, firmware_traces, flush (no-op), db_ready?
17
+ - `create_store` in `Trace` module now selects PostgresStore when a PostgreSQL or MySQL connection is available with both required tables; falls back to CacheStore or in-memory Store
18
+ - `postgres_available?` and `resolve_tenant_id` private helpers on `Trace` module
19
+ - 46 new specs covering all PostgresStore methods using an in-memory SQLite DB
20
+
3
21
  ## [0.1.9] - 2026-03-25
4
22
 
5
23
  ### Added
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Memory
7
+ module Trace
8
+ module Helpers
9
+ module HotTier
10
+ HOT_TTL = 86_400 # 24 hours
11
+
12
+ module_function
13
+
14
+ # Cache a trace in the Redis hot tier.
15
+ def cache_trace(trace, tenant_id: nil)
16
+ return unless available?
17
+
18
+ tid = tenant_id || trace[:partition_id]
19
+ key = trace_key(tid, trace[:trace_id])
20
+ data = serialize_trace(trace)
21
+ Legion::Cache::RedisHash.hset(key, data)
22
+ Legion::Cache::RedisHash.expire(key, HOT_TTL)
23
+
24
+ index_key = "legion:tier:hot:#{tid}"
25
+ Legion::Cache::RedisHash.zadd(index_key, Time.now.to_f, trace[:trace_id])
26
+ end
27
+
28
+ # Fetch a trace from the hot tier. Returns a deserialized trace hash or nil on miss.
29
+ def fetch_trace(trace_id, tenant_id: nil)
30
+ return nil unless available?
31
+
32
+ key = trace_key(tenant_id, trace_id)
33
+ data = Legion::Cache::RedisHash.hgetall(key)
34
+ return nil if data.nil? || data.empty?
35
+
36
+ deserialize_trace(data)
37
+ end
38
+
39
+ # Evict a trace from the hot tier and remove it from the sorted-set index.
40
+ def evict_trace(trace_id, tenant_id: nil)
41
+ return unless available?
42
+
43
+ key = trace_key(tenant_id, trace_id)
44
+ Legion::Cache.delete(key)
45
+
46
+ index_key = "legion:tier:hot:#{tenant_id}"
47
+ Legion::Cache::RedisHash.zrem(index_key, trace_id)
48
+ end
49
+
50
+ # Returns true when the RedisHash module is loaded and Redis is reachable.
51
+ def available?
52
+ defined?(Legion::Cache::RedisHash) &&
53
+ Legion::Cache::RedisHash.redis_available?
54
+ rescue StandardError
55
+ false
56
+ end
57
+
58
+ # Build the namespaced Redis key for a trace.
59
+ def trace_key(tenant_id, trace_id)
60
+ "legion:trace:#{tenant_id}:#{trace_id}"
61
+ end
62
+
63
+ # Serialize a trace hash to a string-only flat hash suitable for Redis HSET.
64
+ def serialize_trace(trace)
65
+ {
66
+ 'trace_id' => trace[:trace_id].to_s,
67
+ 'trace_type' => trace[:trace_type].to_s,
68
+ 'content_payload' => trace[:content_payload].to_s,
69
+ 'strength' => trace[:strength].to_s,
70
+ 'peak_strength' => trace[:peak_strength].to_s,
71
+ 'confidence' => trace[:confidence].to_s,
72
+ 'storage_tier' => 'hot',
73
+ 'partition_id' => trace[:partition_id].to_s,
74
+ 'last_reinforced' => (trace[:last_reinforced] || Time.now).to_s
75
+ }
76
+ end
77
+
78
+ # Deserialize a Redis string-hash back to a typed trace hash.
79
+ def deserialize_trace(data)
80
+ {
81
+ trace_id: data['trace_id'],
82
+ trace_type: data['trace_type']&.to_sym,
83
+ content_payload: data['content_payload'],
84
+ strength: data['strength']&.to_f,
85
+ peak_strength: data['peak_strength']&.to_f,
86
+ confidence: data['confidence']&.to_f,
87
+ storage_tier: :hot,
88
+ partition_id: data['partition_id'],
89
+ last_reinforced: data['last_reinforced']
90
+ }
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,393 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'hot_tier'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Agentic
8
+ module Memory
9
+ module Trace
10
+ module Helpers
11
+ # Write-through durable store backed by Legion::Data (PostgreSQL or MySQL).
12
+ # All writes go directly to the database — no in-memory dirty tracking, no flush.
13
+ # Scoped by tenant_id so multiple agents can share the same DB tables safely.
14
+ class PostgresStore
15
+ TRACES_TABLE = :memory_traces
16
+ ASSOCIATIONS_TABLE = :memory_associations
17
+
18
+ def initialize(tenant_id: nil)
19
+ @tenant_id = tenant_id
20
+ end
21
+
22
+ # Store (upsert) a trace by trace_id.
23
+ # Returns the trace_id on success, nil if the DB is not ready.
24
+ def store(trace)
25
+ return nil unless db_ready?
26
+
27
+ row = serialize_trace(trace)
28
+ begin
29
+ db[TRACES_TABLE].insert_conflict(:replace).insert(row)
30
+ rescue Sequel::UniqueConstraintViolation
31
+ db[TRACES_TABLE].where(trace_id: trace[:trace_id]).update(row.except(:trace_id))
32
+ end
33
+ HotTier.cache_trace(trace, tenant_id: @tenant_id) if HotTier.available?
34
+ trace[:trace_id]
35
+ rescue StandardError => e
36
+ log_warn("store failed: #{e.message}")
37
+ nil
38
+ end
39
+
40
+ # Retrieve a single trace by trace_id (tenant-scoped).
41
+ # Checks the Redis hot tier first; falls through to DB on a miss and caches the result.
42
+ # Returns a trace hash or nil.
43
+ def retrieve(trace_id)
44
+ if HotTier.available?
45
+ cached = HotTier.fetch_trace(trace_id, tenant_id: @tenant_id)
46
+ return cached if cached
47
+ end
48
+
49
+ return nil unless db_ready?
50
+
51
+ row = traces_ds.where(trace_id: trace_id).first
52
+ trace = row ? deserialize_trace(row) : nil
53
+ HotTier.cache_trace(trace, tenant_id: @tenant_id) if HotTier.available? && trace
54
+ trace
55
+ rescue StandardError => e
56
+ log_warn("retrieve failed: #{e.message}")
57
+ nil
58
+ end
59
+
60
+ # Retrieve traces by type, ordered by strength descending.
61
+ def retrieve_by_type(type, limit: 100, min_strength: 0.0)
62
+ return [] unless db_ready?
63
+
64
+ rows = traces_ds
65
+ .where(trace_type: type.to_s)
66
+ .where { strength >= min_strength }
67
+ .order(Sequel.desc(:strength))
68
+ .limit(limit)
69
+ .all
70
+ rows.map { |r| deserialize_trace(r) }
71
+ rescue StandardError => e
72
+ log_warn("retrieve_by_type failed: #{e.message}")
73
+ []
74
+ end
75
+
76
+ # Retrieve traces whose domain_tags column contains the given tag string.
77
+ def retrieve_by_domain(tag, limit: 50)
78
+ return [] unless db_ready?
79
+
80
+ rows = traces_ds
81
+ .where(Sequel.like(:domain_tags, "%#{tag}%"))
82
+ .order(Sequel.desc(:strength))
83
+ .limit(limit)
84
+ .all
85
+ rows.map { |r| deserialize_trace(r) }
86
+ rescue StandardError => e
87
+ log_warn("retrieve_by_domain failed: #{e.message}")
88
+ []
89
+ end
90
+
91
+ # Return all traces for this tenant.
92
+ def all_traces
93
+ return [] unless db_ready?
94
+
95
+ traces_ds.all.map { |r| deserialize_trace(r) }
96
+ rescue StandardError => e
97
+ log_warn("all_traces failed: #{e.message}")
98
+ []
99
+ end
100
+
101
+ # Delete a trace and its association rows.
102
+ def delete(trace_id)
103
+ HotTier.evict_trace(trace_id, tenant_id: @tenant_id) if HotTier.available?
104
+ return unless db_ready?
105
+
106
+ db[ASSOCIATIONS_TABLE].where(trace_id_a: trace_id).delete
107
+ db[ASSOCIATIONS_TABLE].where(trace_id_b: trace_id).delete
108
+ db[TRACES_TABLE].where(trace_id: trace_id).delete
109
+ rescue StandardError => e
110
+ log_warn("delete failed: #{e.message}")
111
+ end
112
+
113
+ # Partial update of a trace by trace_id.
114
+ # Evicts the hot-tier entry so a stale cached version cannot be served.
115
+ def update(trace_id, **fields)
116
+ return unless db_ready?
117
+
118
+ db[TRACES_TABLE].where(trace_id: trace_id).update(map_update_fields(fields))
119
+ HotTier.evict_trace(trace_id, tenant_id: @tenant_id) if HotTier.available?
120
+ rescue StandardError => e
121
+ log_warn("update failed: #{e.message}")
122
+ end
123
+
124
+ # Create or increment a coactivation association between two traces.
125
+ def record_coactivation(id_a, id_b)
126
+ return unless db_ready?
127
+ return if id_a == id_b
128
+
129
+ now = Time.now.utc
130
+ existing = db[ASSOCIATIONS_TABLE]
131
+ .where(trace_id_a: id_a, trace_id_b: id_b)
132
+ .first
133
+
134
+ if existing
135
+ db[ASSOCIATIONS_TABLE]
136
+ .where(id: existing[:id])
137
+ .update(
138
+ coactivation_count: existing[:coactivation_count] + 1,
139
+ updated_at: now
140
+ )
141
+ else
142
+ db[ASSOCIATIONS_TABLE].insert(
143
+ trace_id_a: id_a,
144
+ trace_id_b: id_b,
145
+ coactivation_count: 1,
146
+ linked: false,
147
+ tenant_id: @tenant_id,
148
+ created_at: now,
149
+ updated_at: now
150
+ )
151
+ end
152
+ rescue StandardError => e
153
+ log_warn("record_coactivation failed: #{e.message}")
154
+ end
155
+
156
+ # Return the set of trace IDs associated with a given trace (bidirectional).
157
+ def associations_for(trace_id)
158
+ return [] unless db_ready?
159
+
160
+ a_side = db[ASSOCIATIONS_TABLE]
161
+ .where(trace_id_a: trace_id)
162
+ .select_map(:trace_id_b)
163
+ b_side = db[ASSOCIATIONS_TABLE]
164
+ .where(trace_id_b: trace_id)
165
+ .select_map(:trace_id_a)
166
+ (a_side + b_side).uniq
167
+ rescue StandardError => e
168
+ log_warn("associations_for failed: #{e.message}")
169
+ []
170
+ end
171
+
172
+ # BFS traversal starting from start_id.
173
+ # Returns an array of { trace_id:, depth:, path: } hashes.
174
+ def walk_associations(start_id:, max_hops: 12, min_strength: 0.1)
175
+ return [] unless db_ready?
176
+
177
+ start_row = traces_ds.where(trace_id: start_id).first
178
+ return [] unless start_row
179
+
180
+ results = []
181
+ visited = Set.new([start_id])
182
+ queue = [[start_id, 0, [start_id]]]
183
+
184
+ until queue.empty?
185
+ current_id, depth, path = queue.shift
186
+ neighbor_ids = associations_for(current_id)
187
+
188
+ neighbor_ids.each do |nid|
189
+ next if visited.include?(nid)
190
+
191
+ neighbor_row = traces_ds
192
+ .where(trace_id: nid)
193
+ .where { strength >= min_strength }
194
+ .first
195
+ next unless neighbor_row
196
+
197
+ visited << nid
198
+ neighbor_path = path + [nid]
199
+ results << { trace_id: nid, depth: depth + 1, path: neighbor_path }
200
+ queue << [nid, depth + 1, neighbor_path] if depth + 1 < max_hops
201
+ end
202
+ end
203
+
204
+ results
205
+ rescue StandardError => e
206
+ log_warn("walk_associations failed: #{e.message}")
207
+ []
208
+ end
209
+
210
+ # Delete the N traces with the lowest confidence for a given type (quota enforcement).
211
+ def delete_lowest_confidence(trace_type:, count:)
212
+ return unless db_ready?
213
+
214
+ ids = traces_ds
215
+ .where(trace_type: trace_type.to_s)
216
+ .order(:confidence)
217
+ .limit(count)
218
+ .select_map(:trace_id)
219
+
220
+ ids.each { |tid| delete(tid) }
221
+ rescue StandardError => e
222
+ log_warn("delete_lowest_confidence failed: #{e.message}")
223
+ end
224
+
225
+ # Delete the N least-recently-used traces for a given type (quota enforcement).
226
+ def delete_least_recently_used(trace_type:, count:)
227
+ return unless db_ready?
228
+
229
+ ids = traces_ds
230
+ .where(trace_type: trace_type.to_s)
231
+ .order(:last_reinforced)
232
+ .limit(count)
233
+ .select_map(:trace_id)
234
+
235
+ ids.each { |tid| delete(tid) }
236
+ rescue StandardError => e
237
+ log_warn("delete_least_recently_used failed: #{e.message}")
238
+ end
239
+
240
+ # Convenience: retrieve firmware-type traces.
241
+ def firmware_traces
242
+ retrieve_by_type(:firmware)
243
+ end
244
+
245
+ # No-op — this store is write-through; nothing to flush.
246
+ def flush; end
247
+
248
+ # Returns true when both required tables exist in the connected DB.
249
+ def db_ready?
250
+ defined?(Legion::Data) &&
251
+ Legion::Data.respond_to?(:connection) &&
252
+ Legion::Data.connection&.table_exists?(TRACES_TABLE) &&
253
+ Legion::Data.connection.table_exists?(ASSOCIATIONS_TABLE)
254
+ rescue StandardError
255
+ false
256
+ end
257
+
258
+ private
259
+
260
+ def db
261
+ Legion::Data.connection
262
+ end
263
+
264
+ # Dataset for memory_traces scoped by tenant_id (if set).
265
+ def traces_ds
266
+ ds = db[TRACES_TABLE]
267
+ @tenant_id ? ds.where(tenant_id: @tenant_id) : ds
268
+ end
269
+
270
+ def serialize_trace(trace)
271
+ payload = trace[:content_payload] || trace[:content]
272
+ tags = trace[:domain_tags]
273
+ assocs = trace[:associated_traces]
274
+ conf = trace[:confidence]
275
+ ev = trace[:emotional_valence]
276
+
277
+ {
278
+ trace_id: trace[:trace_id],
279
+ tenant_id: @tenant_id,
280
+ trace_type: trace[:trace_type].to_s,
281
+ content: payload.is_a?(Hash) ? Legion::JSON.dump(payload) : payload.to_s,
282
+ significance: conf,
283
+ confidence: conf,
284
+ associations: assocs.is_a?(Array) ? Legion::JSON.dump(assocs) : '[]',
285
+ domain_tags: tags.is_a?(Array) ? Legion::JSON.dump(tags) : nil,
286
+ strength: trace[:strength],
287
+ peak_strength: trace[:peak_strength],
288
+ base_decay_rate: trace[:base_decay_rate],
289
+ emotional_valence: ev.is_a?(Numeric) ? ev.to_f : 0.0,
290
+ emotional_intensity: trace[:emotional_intensity],
291
+ origin: trace[:origin].to_s,
292
+ source_agent_id: trace[:source_agent_id],
293
+ storage_tier: trace[:storage_tier].to_s,
294
+ last_reinforced: trace[:last_reinforced],
295
+ last_decayed: trace[:last_decayed],
296
+ reinforcement_count: trace[:reinforcement_count],
297
+ unresolved: trace[:unresolved] || false,
298
+ consolidation_candidate: trace[:consolidation_candidate] || false,
299
+ parent_trace_id: trace[:parent_trace_id],
300
+ encryption_key_id: trace[:encryption_key_id],
301
+ partition_id: trace[:partition_id],
302
+ created_at: trace[:created_at] || Time.now.utc,
303
+ accessed_at: Time.now.utc
304
+ }
305
+ end
306
+
307
+ def deserialize_trace(row)
308
+ content = parse_json_or_raw(row[:content])
309
+ {
310
+ trace_id: row[:trace_id],
311
+ trace_type: row[:trace_type]&.to_sym,
312
+ content_payload: content,
313
+ content: content,
314
+ strength: row[:strength],
315
+ peak_strength: row[:peak_strength],
316
+ base_decay_rate: row[:base_decay_rate],
317
+ emotional_valence: row[:emotional_valence].to_f,
318
+ emotional_intensity: row[:emotional_intensity],
319
+ domain_tags: parse_json_array(row[:domain_tags]),
320
+ origin: row[:origin]&.to_sym,
321
+ source_agent_id: row[:source_agent_id],
322
+ created_at: row[:created_at],
323
+ last_reinforced: row[:last_reinforced],
324
+ last_decayed: row[:last_decayed],
325
+ reinforcement_count: row[:reinforcement_count],
326
+ confidence: row[:confidence],
327
+ storage_tier: row[:storage_tier]&.to_sym,
328
+ partition_id: row[:partition_id],
329
+ encryption_key_id: row[:encryption_key_id],
330
+ associated_traces: parse_json_array(row[:associations]),
331
+ parent_trace_id: row[:parent_trace_id],
332
+ child_trace_ids: [],
333
+ unresolved: row[:unresolved] || false,
334
+ consolidation_candidate: row[:consolidation_candidate] || false
335
+ }
336
+ end
337
+
338
+ # Map keyword fields for partial updates, translating to DB column names.
339
+ def map_update_fields(fields)
340
+ mapping = {
341
+ content_payload: :content,
342
+ associated_traces: :associations,
343
+ parent_trace_id: :parent_trace_id,
344
+ child_trace_ids: nil # not stored as a column
345
+ }
346
+
347
+ fields.each_with_object({}) do |(k, v), row|
348
+ col = mapping.key?(k) ? mapping[k] : k
349
+ next if col.nil?
350
+
351
+ row[col] = case col
352
+ when :content
353
+ v.is_a?(Hash) ? Legion::JSON.dump(v) : v.to_s
354
+ when :associations
355
+ v.is_a?(Array) ? Legion::JSON.dump(v) : '[]'
356
+ when :domain_tags
357
+ v.is_a?(Array) ? Legion::JSON.dump(v) : nil
358
+ when :trace_type, :origin, :storage_tier
359
+ v.to_s
360
+ else
361
+ v
362
+ end
363
+ end
364
+ end
365
+
366
+ def parse_json_or_raw(raw)
367
+ return raw unless raw.is_a?(String)
368
+
369
+ parsed = Legion::JSON.load(raw)
370
+ parsed.is_a?(Hash) ? parsed : raw
371
+ rescue StandardError
372
+ raw
373
+ end
374
+
375
+ def parse_json_array(raw)
376
+ return [] unless raw.is_a?(String)
377
+
378
+ result = Legion::JSON.load(raw)
379
+ result.is_a?(Array) ? result : []
380
+ rescue StandardError
381
+ []
382
+ end
383
+
384
+ def log_warn(message)
385
+ Legion::Logging.warn "[memory:postgres_store] #{message}" if defined?(Legion::Logging)
386
+ end
387
+ end
388
+ end
389
+ end
390
+ end
391
+ end
392
+ end
393
+ end
@@ -5,6 +5,7 @@ require 'legion/extensions/agentic/memory/trace/helpers/trace'
5
5
  require 'legion/extensions/agentic/memory/trace/helpers/decay'
6
6
  require 'legion/extensions/agentic/memory/trace/helpers/store'
7
7
  require 'legion/extensions/agentic/memory/trace/helpers/cache_store'
8
+ require 'legion/extensions/agentic/memory/trace/helpers/postgres_store'
8
9
  require 'legion/extensions/agentic/memory/trace/helpers/error_tracer'
9
10
  require 'legion/extensions/agentic/memory/trace/runners/traces'
10
11
  require 'legion/extensions/agentic/memory/trace/runners/consolidation'
@@ -31,7 +32,10 @@ module Legion
31
32
  private
32
33
 
33
34
  def create_store
34
- if defined?(Legion::Cache) && Legion::Cache.respond_to?(:connected?) && Legion::Cache.connected?
35
+ if postgres_available?
36
+ Legion::Logging.debug '[memory] Using shared PostgresStore (write-through)'
37
+ Helpers::PostgresStore.new(tenant_id: resolve_tenant_id)
38
+ elsif defined?(Legion::Cache) && Legion::Cache.respond_to?(:connected?) && Legion::Cache.connected?
35
39
  Legion::Logging.debug '[memory] Using shared CacheStore (memcached)'
36
40
  Helpers::CacheStore.new
37
41
  else
@@ -39,6 +43,23 @@ module Legion
39
43
  Helpers::Store.new
40
44
  end
41
45
  end
46
+
47
+ def postgres_available?
48
+ defined?(Legion::Data) &&
49
+ Legion::Data.respond_to?(:connection) &&
50
+ Legion::Data.connection &&
51
+ %i[postgres mysql2].include?(Legion::Data.connection.adapter_scheme) &&
52
+ Legion::Data.connection.table_exists?(:memory_traces) &&
53
+ Legion::Data.connection.table_exists?(:memory_associations)
54
+ rescue StandardError
55
+ false
56
+ end
57
+
58
+ def resolve_tenant_id
59
+ Legion::Settings[:data]&.dig(:tenant_id)
60
+ rescue StandardError
61
+ nil
62
+ end
42
63
  end
43
64
  end
44
65
  end
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Agentic
6
6
  module Memory
7
- VERSION = '0.1.9'
7
+ VERSION = '0.1.11'
8
8
  end
9
9
  end
10
10
  end