lex-agentic-memory 0.1.8 → 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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -1
  3. data/lib/legion/extensions/agentic/memory/archaeology/actors/decay.rb +22 -0
  4. data/lib/legion/extensions/agentic/memory/archaeology/runners/cognitive_archaeology.rb +6 -0
  5. data/lib/legion/extensions/agentic/memory/compression/actors/maintenance.rb +22 -0
  6. data/lib/legion/extensions/agentic/memory/echo/actors/decay.rb +22 -0
  7. data/lib/legion/extensions/agentic/memory/echo_chamber/actors/decay.rb +22 -0
  8. data/lib/legion/extensions/agentic/memory/echo_chamber/runners/cognitive_echo_chamber.rb +6 -0
  9. data/lib/legion/extensions/agentic/memory/immune_memory/actors/decay.rb +22 -0
  10. data/lib/legion/extensions/agentic/memory/nostalgia/actors/maintenance.rb +22 -0
  11. data/lib/legion/extensions/agentic/memory/palimpsest/actors/decay.rb +22 -0
  12. data/lib/legion/extensions/agentic/memory/reserve/actors/maintenance.rb +22 -0
  13. data/lib/legion/extensions/agentic/memory/semantic_priming/actors/decay.rb +22 -0
  14. data/lib/legion/extensions/agentic/memory/semantic_satiation/actors/recovery.rb +22 -0
  15. data/lib/legion/extensions/agentic/memory/trace/actors/quota.rb +22 -0
  16. data/lib/legion/extensions/agentic/memory/trace/helpers/hot_tier.rb +98 -0
  17. data/lib/legion/extensions/agentic/memory/trace/helpers/postgres_store.rb +393 -0
  18. data/lib/legion/extensions/agentic/memory/trace/runners/consolidation.rb +7 -0
  19. data/lib/legion/extensions/agentic/memory/trace.rb +22 -1
  20. data/lib/legion/extensions/agentic/memory/version.rb +1 -1
  21. data/spec/legion/extensions/agentic/memory/archaeology/actors/decay_spec.rb +24 -0
  22. data/spec/legion/extensions/agentic/memory/compression/actors/maintenance_spec.rb +24 -0
  23. data/spec/legion/extensions/agentic/memory/echo/actors/decay_spec.rb +24 -0
  24. data/spec/legion/extensions/agentic/memory/echo_chamber/actors/decay_spec.rb +24 -0
  25. data/spec/legion/extensions/agentic/memory/immune_memory/actors/decay_spec.rb +24 -0
  26. data/spec/legion/extensions/agentic/memory/nostalgia/actors/maintenance_spec.rb +24 -0
  27. data/spec/legion/extensions/agentic/memory/palimpsest/actors/decay_spec.rb +24 -0
  28. data/spec/legion/extensions/agentic/memory/reserve/actors/maintenance_spec.rb +24 -0
  29. data/spec/legion/extensions/agentic/memory/semantic_priming/actors/decay_spec.rb +24 -0
  30. data/spec/legion/extensions/agentic/memory/semantic_satiation/actors/recovery_spec.rb +24 -0
  31. data/spec/legion/extensions/agentic/memory/trace/actors/quota_spec.rb +24 -0
  32. data/spec/legion/extensions/agentic/memory/trace/helpers/hot_tier_spec.rb +337 -0
  33. data/spec/legion/extensions/agentic/memory/trace/helpers/postgres_store_spec.rb +464 -0
  34. metadata +27 -1
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Actors
6
+ class Every # rubocop:disable Lint/EmptyClass
7
+ end
8
+ end
9
+ end
10
+ end
11
+ $LOADED_FEATURES << 'legion/extensions/actors/every'
12
+
13
+ require 'legion/extensions/agentic/memory/semantic_priming/actors/decay'
14
+
15
+ RSpec.describe Legion::Extensions::Agentic::Memory::SemanticPriming::Actors::Decay do
16
+ subject(:actor) { described_class.new }
17
+
18
+ it { expect(actor.runner_class).to eq(Legion::Extensions::Agentic::Memory::SemanticPriming::Runners::SemanticPriming) }
19
+ it { expect(actor.runner_function).to eq('decay') }
20
+ it { expect(actor.time).to eq(30) }
21
+ it { expect(actor.use_runner?).to be false }
22
+ it { expect(actor.check_subtask?).to be false }
23
+ it { expect(actor.generate_task?).to be false }
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Actors
6
+ class Every # rubocop:disable Lint/EmptyClass
7
+ end
8
+ end
9
+ end
10
+ end
11
+ $LOADED_FEATURES << 'legion/extensions/actors/every'
12
+
13
+ require 'legion/extensions/agentic/memory/semantic_satiation/actors/recovery'
14
+
15
+ RSpec.describe Legion::Extensions::Agentic::Memory::SemanticSatiation::Actors::Recovery do
16
+ subject(:actor) { described_class.new }
17
+
18
+ it { expect(actor.runner_class).to eq(Legion::Extensions::Agentic::Memory::SemanticSatiation::Runners::SemanticSatiation) }
19
+ it { expect(actor.runner_function).to eq('recover') }
20
+ it { expect(actor.time).to eq(60) }
21
+ it { expect(actor.use_runner?).to be false }
22
+ it { expect(actor.check_subtask?).to be false }
23
+ it { expect(actor.generate_task?).to be false }
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Actors
6
+ class Every # rubocop:disable Lint/EmptyClass
7
+ end
8
+ end
9
+ end
10
+ end
11
+ $LOADED_FEATURES << 'legion/extensions/actors/every'
12
+
13
+ require 'legion/extensions/agentic/memory/trace/actors/quota'
14
+
15
+ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Actor::Quota do
16
+ subject(:actor) { described_class.new }
17
+
18
+ it { expect(actor.runner_class).to eq(Legion::Extensions::Agentic::Memory::Trace::Runners::Consolidation) }
19
+ it { expect(actor.runner_function).to eq('enforce_quota') }
20
+ it { expect(actor.time).to eq(300) }
21
+ it { expect(actor.use_runner?).to be false }
22
+ it { expect(actor.check_subtask?).to be false }
23
+ it { expect(actor.generate_task?).to be false }
24
+ end
@@ -0,0 +1,337 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ # Legion::Cache::RedisHash lives in legion-cache. The installed gem version may not have it
6
+ # yet, so we define a minimal stub for testing HotTier in isolation.
7
+ unless defined?(Legion::Cache::RedisHash)
8
+ module Legion
9
+ module Cache
10
+ module RedisHash
11
+ module_function
12
+
13
+ def redis_available? = false
14
+ def hset(_key, _hash) = false
15
+ def hgetall(_key) = nil
16
+ def hdel(_key, *_fields) = 0
17
+ def zadd(_key, _score, _member) = false
18
+ def zrangebyscore(_key, _min, _max, **) = []
19
+ def zrem(_key, _member) = false
20
+ def expire(_key, _seconds) = false
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::HotTier do
27
+ subject(:mod) { described_class }
28
+
29
+ let(:tenant_id) { 'tenant-abc' }
30
+ let(:trace_id) { 'trace-uuid-001' }
31
+
32
+ let(:trace) do
33
+ {
34
+ trace_id: trace_id,
35
+ trace_type: :semantic,
36
+ content_payload: 'ruby is great',
37
+ strength: 0.85,
38
+ peak_strength: 0.95,
39
+ confidence: 0.9,
40
+ partition_id: tenant_id,
41
+ last_reinforced: Time.now
42
+ }
43
+ end
44
+
45
+ # --- available? ---
46
+
47
+ describe '.available?' do
48
+ context 'when Legion::Cache::RedisHash is not defined' do
49
+ it 'returns a falsy value gracefully' do
50
+ hide_const('Legion::Cache::RedisHash')
51
+ expect(mod.available?).to be_falsy
52
+ end
53
+ end
54
+
55
+ context 'when Legion::Cache::RedisHash is defined but not connected' do
56
+ before do
57
+ stub_const('Legion::Cache::RedisHash', Module.new)
58
+ allow(Legion::Cache::RedisHash).to receive(:redis_available?).and_return(false)
59
+ end
60
+
61
+ it 'returns false' do
62
+ expect(mod.available?).to be false
63
+ end
64
+ end
65
+
66
+ context 'when Legion::Cache::RedisHash is defined and connected' do
67
+ before do
68
+ stub_const('Legion::Cache::RedisHash', Module.new)
69
+ allow(Legion::Cache::RedisHash).to receive(:redis_available?).and_return(true)
70
+ end
71
+
72
+ it 'returns true' do
73
+ expect(mod.available?).to be true
74
+ end
75
+ end
76
+
77
+ context 'when an exception is raised' do
78
+ before { allow(mod).to receive(:available?).and_call_original }
79
+
80
+ it 'returns false without raising' do
81
+ stub_const('Legion::Cache::RedisHash', Module.new)
82
+ allow(Legion::Cache::RedisHash).to receive(:redis_available?).and_raise(RuntimeError, 'boom')
83
+ expect(mod.available?).to be false
84
+ end
85
+ end
86
+ end
87
+
88
+ # --- trace_key ---
89
+
90
+ describe '.trace_key' do
91
+ it 'builds a namespaced key from tenant and trace id' do
92
+ expect(mod.trace_key('tenant-1', 'trace-2')).to eq('legion:trace:tenant-1:trace-2')
93
+ end
94
+
95
+ it 'handles nil tenant gracefully' do
96
+ expect(mod.trace_key(nil, 'trace-2')).to eq('legion:trace::trace-2')
97
+ end
98
+ end
99
+
100
+ # --- serialize_trace / deserialize_trace round-trip ---
101
+
102
+ describe '.serialize_trace' do
103
+ subject(:serialized) { mod.serialize_trace(trace) }
104
+
105
+ it 'returns a Hash with string keys only' do
106
+ expect(serialized.keys).to all(be_a(String))
107
+ end
108
+
109
+ it 'includes all expected fields' do
110
+ expect(serialized.keys).to include(
111
+ 'trace_id', 'trace_type', 'content_payload',
112
+ 'strength', 'peak_strength', 'confidence',
113
+ 'storage_tier', 'partition_id', 'last_reinforced'
114
+ )
115
+ end
116
+
117
+ it 'sets storage_tier to "hot"' do
118
+ expect(serialized['storage_tier']).to eq('hot')
119
+ end
120
+
121
+ it 'converts trace_id to string' do
122
+ expect(serialized['trace_id']).to eq(trace_id)
123
+ end
124
+
125
+ it 'converts trace_type to string' do
126
+ expect(serialized['trace_type']).to eq('semantic')
127
+ end
128
+
129
+ it 'converts numeric fields to strings' do
130
+ expect(serialized['strength']).to eq('0.85')
131
+ expect(serialized['peak_strength']).to eq('0.95')
132
+ expect(serialized['confidence']).to eq('0.9')
133
+ end
134
+
135
+ it 'uses Time.now when last_reinforced is nil' do
136
+ t = mod.serialize_trace(trace.merge(last_reinforced: nil))
137
+ expect(t['last_reinforced']).not_to be_empty
138
+ end
139
+ end
140
+
141
+ describe '.deserialize_trace' do
142
+ let(:data) do
143
+ {
144
+ 'trace_id' => trace_id,
145
+ 'trace_type' => 'semantic',
146
+ 'content_payload' => 'ruby is great',
147
+ 'strength' => '0.85',
148
+ 'peak_strength' => '0.95',
149
+ 'confidence' => '0.9',
150
+ 'storage_tier' => 'hot',
151
+ 'partition_id' => tenant_id,
152
+ 'last_reinforced' => Time.now.to_s
153
+ }
154
+ end
155
+
156
+ subject(:deserialized) { mod.deserialize_trace(data) }
157
+
158
+ it 'returns a hash with symbol keys' do
159
+ expect(deserialized).to be_a(Hash)
160
+ expect(deserialized.keys).to all(be_a(Symbol))
161
+ end
162
+
163
+ it 'sets trace_id correctly' do
164
+ expect(deserialized[:trace_id]).to eq(trace_id)
165
+ end
166
+
167
+ it 'converts trace_type back to a symbol' do
168
+ expect(deserialized[:trace_type]).to eq(:semantic)
169
+ end
170
+
171
+ it 'converts numeric strings back to floats' do
172
+ expect(deserialized[:strength]).to be_within(0.001).of(0.85)
173
+ expect(deserialized[:peak_strength]).to be_within(0.001).of(0.95)
174
+ expect(deserialized[:confidence]).to be_within(0.001).of(0.9)
175
+ end
176
+
177
+ it 'forces storage_tier to :hot' do
178
+ expect(deserialized[:storage_tier]).to eq(:hot)
179
+ end
180
+
181
+ it 'preserves partition_id' do
182
+ expect(deserialized[:partition_id]).to eq(tenant_id)
183
+ end
184
+ end
185
+
186
+ describe 'serialize -> deserialize round-trip' do
187
+ it 'recovers numeric values from serialized form' do
188
+ serialized = mod.serialize_trace(trace)
189
+ deserialized = mod.deserialize_trace(serialized)
190
+
191
+ expect(deserialized[:trace_id]).to eq(trace[:trace_id])
192
+ expect(deserialized[:trace_type]).to eq(trace[:trace_type])
193
+ expect(deserialized[:strength]).to be_within(0.001).of(trace[:strength])
194
+ expect(deserialized[:confidence]).to be_within(0.001).of(trace[:confidence])
195
+ expect(deserialized[:storage_tier]).to eq(:hot)
196
+ end
197
+ end
198
+
199
+ # --- cache_trace ---
200
+
201
+ describe '.cache_trace' do
202
+ context 'when unavailable' do
203
+ before { allow(mod).to receive(:available?).and_return(false) }
204
+
205
+ it 'returns nil without calling RedisHash' do
206
+ expect(Legion::Cache::RedisHash).not_to receive(:hset)
207
+ expect(mod.cache_trace(trace, tenant_id: tenant_id)).to be_nil
208
+ end
209
+ end
210
+
211
+ context 'when available' do
212
+ before do
213
+ allow(mod).to receive(:available?).and_return(true)
214
+ allow(Legion::Cache::RedisHash).to receive(:hset).and_return(true)
215
+ allow(Legion::Cache::RedisHash).to receive(:expire).and_return(true)
216
+ allow(Legion::Cache::RedisHash).to receive(:zadd).and_return(true)
217
+ end
218
+
219
+ it 'calls hset with the correct key and serialized data' do
220
+ key = mod.trace_key(tenant_id, trace_id)
221
+ expect(Legion::Cache::RedisHash).to receive(:hset).with(key, hash_including('trace_id' => trace_id))
222
+ mod.cache_trace(trace, tenant_id: tenant_id)
223
+ end
224
+
225
+ it 'calls expire with HOT_TTL' do
226
+ key = mod.trace_key(tenant_id, trace_id)
227
+ expect(Legion::Cache::RedisHash).to receive(:expire).with(key, described_class::HOT_TTL)
228
+ mod.cache_trace(trace, tenant_id: tenant_id)
229
+ end
230
+
231
+ it 'adds to the sorted-set index' do
232
+ index_key = "legion:tier:hot:#{tenant_id}"
233
+ expect(Legion::Cache::RedisHash).to receive(:zadd).with(index_key, anything, trace_id)
234
+ mod.cache_trace(trace, tenant_id: tenant_id)
235
+ end
236
+
237
+ it 'falls back to partition_id when tenant_id is not provided' do
238
+ key = mod.trace_key(trace[:partition_id], trace_id)
239
+ expect(Legion::Cache::RedisHash).to receive(:hset).with(key, anything)
240
+ mod.cache_trace(trace)
241
+ end
242
+ end
243
+ end
244
+
245
+ # --- fetch_trace ---
246
+
247
+ describe '.fetch_trace' do
248
+ context 'when unavailable' do
249
+ before { allow(mod).to receive(:available?).and_return(false) }
250
+
251
+ it 'returns nil' do
252
+ expect(mod.fetch_trace(trace_id, tenant_id: tenant_id)).to be_nil
253
+ end
254
+ end
255
+
256
+ context 'when available but key is missing' do
257
+ before do
258
+ allow(mod).to receive(:available?).and_return(true)
259
+ allow(Legion::Cache::RedisHash).to receive(:hgetall).and_return({})
260
+ end
261
+
262
+ it 'returns nil' do
263
+ expect(mod.fetch_trace(trace_id, tenant_id: tenant_id)).to be_nil
264
+ end
265
+ end
266
+
267
+ context 'when available and key exists' do
268
+ let(:cached_data) do
269
+ {
270
+ 'trace_id' => trace_id,
271
+ 'trace_type' => 'semantic',
272
+ 'content_payload' => 'ruby is great',
273
+ 'strength' => '0.85',
274
+ 'peak_strength' => '0.95',
275
+ 'confidence' => '0.9',
276
+ 'storage_tier' => 'hot',
277
+ 'partition_id' => tenant_id,
278
+ 'last_reinforced' => Time.now.to_s
279
+ }
280
+ end
281
+
282
+ before do
283
+ allow(mod).to receive(:available?).and_return(true)
284
+ allow(Legion::Cache::RedisHash).to receive(:hgetall).and_return(cached_data)
285
+ end
286
+
287
+ it 'returns a deserialized trace hash' do
288
+ result = mod.fetch_trace(trace_id, tenant_id: tenant_id)
289
+ expect(result).not_to be_nil
290
+ expect(result[:trace_id]).to eq(trace_id)
291
+ expect(result[:trace_type]).to eq(:semantic)
292
+ expect(result[:storage_tier]).to eq(:hot)
293
+ end
294
+ end
295
+ end
296
+
297
+ # --- evict_trace ---
298
+
299
+ describe '.evict_trace' do
300
+ context 'when unavailable' do
301
+ before { allow(mod).to receive(:available?).and_return(false) }
302
+
303
+ it 'returns nil without calling Cache.delete' do
304
+ expect(Legion::Cache).not_to receive(:delete)
305
+ expect(mod.evict_trace(trace_id, tenant_id: tenant_id)).to be_nil
306
+ end
307
+ end
308
+
309
+ context 'when available' do
310
+ before do
311
+ allow(mod).to receive(:available?).and_return(true)
312
+ allow(Legion::Cache).to receive(:delete).and_return(true)
313
+ allow(Legion::Cache::RedisHash).to receive(:zrem).and_return(true)
314
+ end
315
+
316
+ it 'deletes the trace key from Cache' do
317
+ key = mod.trace_key(tenant_id, trace_id)
318
+ expect(Legion::Cache).to receive(:delete).with(key)
319
+ mod.evict_trace(trace_id, tenant_id: tenant_id)
320
+ end
321
+
322
+ it 'removes the entry from the sorted-set index' do
323
+ index_key = "legion:tier:hot:#{tenant_id}"
324
+ expect(Legion::Cache::RedisHash).to receive(:zrem).with(index_key, trace_id)
325
+ mod.evict_trace(trace_id, tenant_id: tenant_id)
326
+ end
327
+ end
328
+ end
329
+
330
+ # --- HOT_TTL constant ---
331
+
332
+ describe 'HOT_TTL' do
333
+ it 'is 86400 (24 hours in seconds)' do
334
+ expect(described_class::HOT_TTL).to eq(86_400)
335
+ end
336
+ end
337
+ end