lex-agentic-memory 0.1.34 → 0.1.36

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: 606b711547732f1f697b57ce2392509c2774397c725b51f87baeaee591cf2ae2
4
- data.tar.gz: f9f704beca9f4f33c8d71ad54627ef400561819898aeebf06c0097a0c0a535b1
3
+ metadata.gz: f135722a81c15e36374e61e6348db1f421db9b4fc5257bc2915d8526a9b7a145
4
+ data.tar.gz: 86dfb884036c9d7d8617a3a785b6b35c6cc0db413b60a9866aad04ad3e899d59
5
5
  SHA512:
6
- metadata.gz: 35f855a01efbfb5b29f23fe12f92e89082a3a4c8001b149dd16f6ccd648bc9d67a699755230e5b08c4a86f1147522f86b71d5fea3e31f3842448c20e4af3d094
7
- data.tar.gz: 8e9afdf79c62437fefe856a61a0432d1574a7fba7dadab51c4a6b19a0e8a6d6a3658a19fbfa725d5cdb5c4c8de300169818aeee15c1e204e9cf7ff15a9cf12d3
6
+ metadata.gz: bd080984039d516a8980ed88723a34f6f8738439f55a921e572cbba852f73af3c48010cffc9fec5b134305f2e7d0f7d774d032bf8a41e88003b9f7cef8cba34a
7
+ data.tar.gz: 5e2fbe8775082a3e64533acce43937923b0e5f6c97cc7bcb623fc4a42f9bc21be513f824fef9ea80f13afb392da5e30648b68fd6e9a9fbadd1e1eeed985259b5
data/CHANGELOG.md CHANGED
@@ -1,10 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.36] - 2026-05-17
4
+ ### Fixed
5
+ - `PostgresStore#db_ready?` now checks `Legion::Data.can_write?(:memory_traces)` before attempting writes, preventing `PG::InsufficientPrivilege` errors when connected with a read-only role.
6
+ - `Trace.postgres_available?` also checks INSERT privilege so `create_store` correctly falls back to a local store instead of selecting PostgresStore and failing at runtime.
7
+
3
8
  ## [0.1.34] - 2026-05-08
4
9
  ### Fixed
5
10
  - Deferred GAIA heartbeat maintenance no longer initializes the shared trace store just to return a deferred summary, avoiding idle local trace table scans.
6
11
  - `Store#parse_db_content` now returns bracket-prefixed raw trace/log text without JSON parse attempts or malformed JSON debug noise while still parsing real JSON objects and arrays.
7
12
 
13
+ ## [0.1.35] - 2026-05-15
14
+ ### Fixed
15
+ - Normalize trace affect fields before persisting through in-memory, cache, local SQLite, and Postgres stores so string-backed values do not crash or silently collapse to `0.0`.
16
+ - Preserve numeric `emotional_valence` across local trace persistence and normalize legacy structured payloads when they are reloaded.
17
+
8
18
  ## [0.1.33] - 2026-05-07
9
19
  ### Fixed
10
20
  - `PostgresStore#parse_json_or_raw` no longer attempts JSON parse on plain-text content — checks for `{`/`[` prefix before parsing, eliminating ERROR log spam during bulk reads with mixed content types
@@ -164,9 +164,9 @@ module Legion
164
164
  site.survey.merge(
165
165
  depth_progress: depth_progress(site),
166
166
  artifact_types: site.artifacts_found
167
- .each_with_object(Hash.new(0)) do |a, h|
168
- h[a.artifact_type] += 1
169
- end
167
+ .each_with_object(Hash.new(0)) do |a, h|
168
+ h[a.artifact_type] += 1
169
+ end
170
170
  )
171
171
  end
172
172
 
@@ -38,11 +38,12 @@ module Legion
38
38
  end
39
39
 
40
40
  def store(trace)
41
+ normalized_trace = Helpers::Trace.normalize_trace_affect(trace)
41
42
  @mutex.synchronize do
42
- @traces[trace[:trace_id]] = trace
43
- @dirty_ids << trace[:trace_id]
43
+ @traces[normalized_trace[:trace_id]] = normalized_trace
44
+ @dirty_ids << normalized_trace[:trace_id]
44
45
  end
45
- trace[:trace_id]
46
+ normalized_trace[:trace_id]
46
47
  end
47
48
 
48
49
  def get(trace_id)
@@ -27,15 +27,16 @@ module Legion
27
27
  def store(trace)
28
28
  return nil unless db_ready?
29
29
 
30
- row = serialize_trace(trace)
30
+ normalized_trace = Helpers::Trace.normalize_trace_affect(trace)
31
+ row = serialize_trace(normalized_trace)
31
32
  ds = db[TRACES_TABLE]
32
33
  if db.adapter_scheme == :mysql2
33
34
  ds.insert_conflict(update: row.except(:trace_id)).insert(row)
34
35
  else
35
36
  ds.insert_conflict(target: :trace_id, update: row.except(:trace_id)).insert(row)
36
37
  end
37
- HotTier.cache_trace(trace, tenant_id: @tenant_id, agent_id: @agent_id) if HotTier.available?
38
- trace[:trace_id]
38
+ HotTier.cache_trace(normalized_trace, tenant_id: @tenant_id, agent_id: @agent_id) if HotTier.available?
39
+ normalized_trace[:trace_id]
39
40
  rescue StandardError => e
40
41
  log_warn("store failed: #{e.message}")
41
42
  nil
@@ -263,12 +264,14 @@ module Legion
263
264
  # No-op — this store is write-through; nothing to flush.
264
265
  def flush; end
265
266
 
266
- # Returns true when both required tables exist in the connected DB.
267
+ # Returns true when both required tables exist and the user has INSERT privilege on both.
267
268
  def db_ready?
268
269
  defined?(Legion::Data) &&
269
270
  Legion::Data.respond_to?(:connection) &&
270
271
  Legion::Data.connection&.table_exists?(TRACES_TABLE) &&
271
- Legion::Data.connection.table_exists?(ASSOCIATIONS_TABLE)
272
+ Legion::Data.connection.table_exists?(ASSOCIATIONS_TABLE) &&
273
+ Legion::Data.can_write?(TRACES_TABLE) &&
274
+ Legion::Data.can_write?(ASSOCIATIONS_TABLE)
272
275
  rescue StandardError => e
273
276
  log.error "[trace_persistence] db_ready?: #{e.message}"
274
277
  false
@@ -299,7 +302,7 @@ module Legion
299
302
  tags = trace[:domain_tags]
300
303
  assocs = trace[:associated_traces]
301
304
  conf = trace[:confidence]
302
- ev = trace[:emotional_valence]
305
+ ev = Helpers::Trace.normalize_emotional_valence(trace[:emotional_valence])
303
306
 
304
307
  {
305
308
  trace_id: trace[:trace_id],
@@ -314,8 +317,8 @@ module Legion
314
317
  strength: trace[:strength],
315
318
  peak_strength: trace[:peak_strength],
316
319
  base_decay_rate: trace[:base_decay_rate],
317
- emotional_valence: ev.is_a?(Numeric) ? ev.to_f : 0.0,
318
- emotional_intensity: trace[:emotional_intensity],
320
+ emotional_valence: ev,
321
+ emotional_intensity: Helpers::Trace.normalize_emotional_intensity(trace[:emotional_intensity]),
319
322
  origin: sanitize_pg_string(trace[:origin].to_s),
320
323
  source_agent_id: sanitize_pg_string(trace[:source_agent_id]),
321
324
  storage_tier: sanitize_pg_string(trace[:storage_tier].to_s),
@@ -29,7 +29,7 @@ module Legion
29
29
  end
30
30
 
31
31
  def store(trace)
32
- persisted_trace = trace.dup
32
+ persisted_trace = Helpers::Trace.normalize_trace_affect(trace)
33
33
  persisted_trace[:partition_id] ||= @partition_id
34
34
  @mutex.synchronize do
35
35
  @traces_dirty = true if @traces[persisted_trace[:trace_id]] != persisted_trace
@@ -120,7 +120,7 @@ module Legion
120
120
  snapshot = Array(traces).each_with_object({}) do |trace, memo|
121
121
  next unless trace.is_a?(Hash) && trace[:trace_id]
122
122
 
123
- restored = trace.dup
123
+ restored = Helpers::Trace.normalize_trace_affect(trace)
124
124
  restored[:partition_id] ||= @partition_id
125
125
  memo[restored[:trace_id]] = restored
126
126
  end
@@ -294,7 +294,7 @@ module Legion
294
294
  strength: trace[:strength],
295
295
  peak_strength: trace[:peak_strength],
296
296
  base_decay_rate: trace[:base_decay_rate],
297
- emotional_valence: trace[:emotional_valence].is_a?(Hash) ? ::JSON.generate(trace[:emotional_valence]) : nil,
297
+ emotional_valence: Helpers::Trace.normalize_emotional_valence(trace[:emotional_valence]).to_s,
298
298
  emotional_intensity: trace[:emotional_intensity],
299
299
  domain_tags: trace[:domain_tags].is_a?(Array) ? ::JSON.generate(trace[:domain_tags]) : nil,
300
300
  origin: trace[:origin].to_s,
@@ -325,7 +325,7 @@ module Legion
325
325
  strength: row[:strength],
326
326
  peak_strength: row[:peak_strength],
327
327
  base_decay_rate: row[:base_decay_rate],
328
- emotional_valence: parse_db_json(row[:emotional_valence], 'emotional_valence', symbolize: true) { 0.0 },
328
+ emotional_valence: deserialize_emotional_valence(row[:emotional_valence]),
329
329
  emotional_intensity: row[:emotional_intensity],
330
330
  domain_tags: parse_db_json(row[:domain_tags], 'domain_tags') { [] },
331
331
  origin: row[:origin]&.to_sym,
@@ -364,6 +364,12 @@ module Legion
364
364
  %w[{ [ " ]].include?(first_array_value)
365
365
  end
366
366
 
367
+ def deserialize_emotional_valence(raw)
368
+ return 0.0 if raw.nil? || raw.to_s.strip.empty?
369
+
370
+ Helpers::Trace.normalize_emotional_valence(raw)
371
+ end
372
+
367
373
  def parse_db_json(raw, field, symbolize: false, &default)
368
374
  return default&.call if raw.nil? || raw.to_s.strip.empty?
369
375
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
3
4
  require 'securerandom'
4
5
 
5
6
  module Legion
@@ -48,6 +49,7 @@ module Legion
48
49
  ASSOCIATION_BONUS = 0.15 # bonus for Hebbian-associated traces
49
50
  MAX_ASSOCIATIONS = 20 # max Hebbian links per trace
50
51
  COACTIVATION_THRESHOLD = 3 # co-activations before Hebbian link forms
52
+ VALENCE_SCALAR_KEYS = %i[valence emotional_valence sentiment polarity score].freeze
51
53
 
52
54
  module_function
53
55
 
@@ -59,6 +61,10 @@ module Legion
59
61
  raise ArgumentError, "invalid origin: #{origin}" unless ORIGINS.include?(origin)
60
62
 
61
63
  now = Time.now.utc
64
+ emotional_context = normalize_trace_affect(
65
+ emotional_valence: emotional_valence,
66
+ emotional_intensity: emotional_intensity
67
+ )
62
68
 
63
69
  {
64
70
  trace_id: SecureRandom.uuid,
@@ -68,8 +74,8 @@ module Legion
68
74
  strength: STARTING_STRENGTHS[type],
69
75
  peak_strength: STARTING_STRENGTHS[type],
70
76
  base_decay_rate: BASE_DECAY_RATES[type],
71
- emotional_valence: emotional_valence.clamp(-1.0, 1.0),
72
- emotional_intensity: emotional_intensity.clamp(0.0, 1.0),
77
+ emotional_valence: emotional_context[:emotional_valence],
78
+ emotional_intensity: emotional_context[:emotional_intensity],
73
79
  domain_tags: Array(domain_tags),
74
80
  origin: origin,
75
81
  source_agent_id: source_agent_id,
@@ -98,11 +104,89 @@ module Legion
98
104
  true
99
105
  end
100
106
 
107
+ def normalize_trace_affect(trace)
108
+ normalized = trace.dup
109
+ normalized[:emotional_valence] = normalize_emotional_valence(normalized[:emotional_valence])
110
+ normalized[:emotional_intensity] = normalize_emotional_intensity(normalized[:emotional_intensity])
111
+ normalized
112
+ end
113
+
114
+ def normalize_emotional_valence(value)
115
+ normalize_scalar(value, min: -1.0, max: 1.0, hash_keys: VALENCE_SCALAR_KEYS)
116
+ end
117
+
118
+ def normalize_emotional_intensity(value)
119
+ normalize_scalar(value, min: 0.0, max: 1.0)
120
+ end
121
+
101
122
  def default_partition_id
102
123
  Legion::Settings.dig(:agent, :id) || 'default'
103
124
  rescue StandardError => _e
104
125
  'default'
105
126
  end
127
+
128
+ def normalize_scalar(value, min:, max:, hash_keys: [])
129
+ case value
130
+ when Numeric
131
+ value.to_f.clamp(min, max)
132
+ when String
133
+ normalize_string_scalar(value, min: min, max: max, hash_keys: hash_keys)
134
+ when Hash
135
+ normalize_hash_scalar(value, min: min, max: max, hash_keys: hash_keys)
136
+ else
137
+ 0.0
138
+ end
139
+ end
140
+
141
+ def normalize_string_scalar(value, min:, max:, hash_keys:)
142
+ stripped = value.strip
143
+ return 0.0 if stripped.empty?
144
+
145
+ Float(stripped).clamp(min, max)
146
+ rescue ArgumentError, TypeError => e
147
+ Legion::Logging.debug("[memory][trace] normalize_string_scalar fallback: #{e.message}")
148
+ parsed = parse_structured_scalar(stripped)
149
+ return normalize_scalar(parsed, min: min, max: max, hash_keys: hash_keys) if parsed
150
+
151
+ 0.0
152
+ end
153
+
154
+ def normalize_hash_scalar(value, min:, max:, hash_keys:)
155
+ symbolized = symbolize_keys(value)
156
+ scalar_value = hash_keys.lazy.map { |key| symbolized[key] }.find { |candidate| scalar_candidate?(candidate) }
157
+ return normalize_scalar(scalar_value, min: min, max: max, hash_keys: hash_keys) if scalar_candidate?(scalar_value)
158
+
159
+ numeric_values = symbolized.values.select { |candidate| scalar_candidate?(candidate) }
160
+ return normalize_scalar(numeric_values.first, min: min, max: max, hash_keys: hash_keys) if numeric_values.one?
161
+
162
+ 0.0
163
+ end
164
+
165
+ def parse_structured_scalar(value)
166
+ return unless value.start_with?('{', '[')
167
+
168
+ ::JSON.parse(value)
169
+ rescue ::JSON::ParserError => e
170
+ Legion::Logging.debug("[memory][trace] parse_structured_scalar ignored: #{e.message}")
171
+ nil
172
+ end
173
+
174
+ def scalar_candidate?(value)
175
+ value.is_a?(Numeric) || value.is_a?(String)
176
+ end
177
+
178
+ def symbolize_keys(value)
179
+ case value
180
+ when Hash
181
+ value.each_with_object({}) do |(key, nested), memo|
182
+ memo[key.to_sym] = symbolize_keys(nested)
183
+ end
184
+ when Array
185
+ value.map { |nested| symbolize_keys(nested) }
186
+ else
187
+ value
188
+ end
189
+ end
106
190
  end
107
191
  end
108
192
  end
@@ -79,7 +79,9 @@ module Legion
79
79
  Legion::Data.connection &&
80
80
  %i[postgres mysql2].include?(Legion::Data.connection.adapter_scheme) &&
81
81
  Legion::Data.connection.table_exists?(:memory_traces) &&
82
- Legion::Data.connection.table_exists?(:memory_associations)
82
+ Legion::Data.connection.table_exists?(:memory_associations) &&
83
+ Legion::Data.can_write?(:memory_traces) &&
84
+ Legion::Data.can_write?(:memory_associations)
83
85
  rescue StandardError => _e
84
86
  false
85
87
  end
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Agentic
6
6
  module Memory
7
- VERSION = '0.1.34'
7
+ VERSION = '0.1.36'
8
8
  end
9
9
  end
10
10
  end
@@ -65,6 +65,9 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Hologram::Helpers::HologramE
65
65
  let(:fragments) { engine.fragment_hologram(hologram_id: hologram.id, count: 4) }
66
66
 
67
67
  it 'returns success: true when sufficient fragments are used' do
68
+ # Enhance all fragments to guarantee they exceed the reconstruction threshold
69
+ # regardless of the random completeness assigned during fragmentation.
70
+ fragments.each { |f| f.enhance!(0.5) }
68
71
  ids = fragments.select(&:sufficient?).map(&:id)
69
72
  result = engine.reconstruct_from_fragments(hologram_id: hologram.id, fragment_ids: ids)
70
73
  expect(result[:success]).to be true
@@ -81,8 +81,11 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::PostgresStor
81
81
  end
82
82
 
83
83
  before do
84
+ allow(Legion::Data).to receive(:respond_to?).and_call_original
84
85
  allow(Legion::Data).to receive(:respond_to?).with(:connection).and_return(true)
85
86
  allow(Legion::Data).to receive(:connection).and_return(db)
87
+ allow(Legion::Data).to receive(:can_write?).with(:memory_traces).and_return(true)
88
+ allow(Legion::Data).to receive(:can_write?).with(:memory_associations).and_return(true)
86
89
  # SQLite adapter_scheme is :sqlite — postgres_available? checks for :postgres/:mysql2,
87
90
  # but PostgresStore itself only calls db_ready? which just needs the tables to exist.
88
91
  allow(db).to receive(:adapter_scheme).and_return(:sqlite)
@@ -109,6 +112,27 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::PostgresStor
109
112
  allow(Legion::Data).to receive(:connection).and_raise(StandardError, 'no db')
110
113
  expect(store.db_ready?).to be false
111
114
  end
115
+
116
+ it 'returns false when user lacks INSERT privilege on memory_traces' do
117
+ allow(Legion::Data).to receive(:can_write?).with(:memory_traces).and_return(false)
118
+ expect(store.db_ready?).to be false
119
+ end
120
+
121
+ it 'returns true when user has INSERT privilege on memory_traces' do
122
+ allow(Legion::Data).to receive(:can_write?).with(:memory_traces).and_return(true)
123
+ expect(store.db_ready?).to be true
124
+ end
125
+
126
+ it 'returns false when user lacks INSERT privilege on memory_associations' do
127
+ allow(Legion::Data).to receive(:can_write?).with(:memory_associations).and_return(false)
128
+ expect(store.db_ready?).to be false
129
+ end
130
+
131
+ it 'returns true when user has INSERT privilege on both tables' do
132
+ allow(Legion::Data).to receive(:can_write?).with(:memory_traces).and_return(true)
133
+ allow(Legion::Data).to receive(:can_write?).with(:memory_associations).and_return(true)
134
+ expect(store.db_ready?).to be true
135
+ end
112
136
  end
113
137
 
114
138
  # --- store insert_conflict syntax ---
@@ -238,6 +262,20 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::PostgresStor
238
262
  end
239
263
  end
240
264
 
265
+ describe 'emotional valence normalization' do
266
+ it 'normalizes string-backed affect fields before persisting' do
267
+ trace = trace_helper.new_trace(type: :episodic, content_payload: { event: 'partner ping' })
268
+ trace[:emotional_valence] = '0.7'
269
+ trace[:emotional_intensity] = '0.9'
270
+
271
+ store.store(trace)
272
+
273
+ row = db[:memory_traces].where(trace_id: trace[:trace_id]).first
274
+ expect(row[:emotional_valence]).to be_within(0.001).of(0.7)
275
+ expect(row[:emotional_intensity]).to be_within(0.001).of(0.9)
276
+ end
277
+ end
278
+
241
279
  # --- retrieve_by_type ---
242
280
 
243
281
  describe '#retrieve_by_type' do
@@ -36,6 +36,30 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::Trace do
36
36
  expect(trace[:emotional_intensity]).to eq(0.0)
37
37
  end
38
38
 
39
+ it 'normalizes string and structured emotional values safely' do
40
+ trace = described_class.new_trace(
41
+ type: :episodic,
42
+ content_payload: { event: 'partner ping' },
43
+ emotional_valence: '{:urgency=>0.6, :importance=>0.8}',
44
+ emotional_intensity: '0.75'
45
+ )
46
+
47
+ expect(trace[:emotional_valence]).to eq(0.0)
48
+ expect(trace[:emotional_intensity]).to eq(0.75)
49
+ end
50
+
51
+ it 'extracts scalar valence from hash payloads when an explicit valence key exists' do
52
+ trace = described_class.new_trace(
53
+ type: :semantic,
54
+ content_payload: { fact: 'test' },
55
+ emotional_valence: { valence: 0.4 },
56
+ emotional_intensity: '2.0'
57
+ )
58
+
59
+ expect(trace[:emotional_valence]).to eq(0.4)
60
+ expect(trace[:emotional_intensity]).to eq(1.0)
61
+ end
62
+
39
63
  it 'rejects invalid trace types' do
40
64
  expect do
41
65
  described_class.new_trace(type: :bogus, content_payload: {})
@@ -249,12 +249,12 @@ RSpec.describe 'lex-memory local SQLite persistence' do
249
249
  expect(fresh.get(episodic_trace[:trace_id])).not_to be_nil
250
250
  end
251
251
 
252
- it 'honors symbolize parsing for JSON hash fields' do
252
+ it 'restores numeric emotional_valence values from local persistence' do
253
253
  trace = trace_helper.new_trace(
254
254
  type: :semantic,
255
255
  content_payload: { fact: 'symbolized' },
256
256
  domain_tags: ['json'],
257
- emotional_valence: { joy: 0.8 }
257
+ emotional_valence: 0.8
258
258
  )
259
259
  store.store(trace)
260
260
  store.save_to_local
@@ -262,8 +262,25 @@ RSpec.describe 'lex-memory local SQLite persistence' do
262
262
  fresh = Legion::Extensions::Agentic::Memory::Trace::Helpers::Store.new
263
263
  restored = fresh.get(trace[:trace_id])
264
264
 
265
- expect(restored[:emotional_valence]).to include(joy: 0.8)
266
- expect(restored[:emotional_valence]).not_to have_key('joy')
265
+ expect(restored[:emotional_valence]).to be_within(0.001).of(0.8)
266
+ end
267
+
268
+ it 'normalizes legacy JSON emotional_valence payloads on load' do
269
+ trace = trace_helper.new_trace(
270
+ type: :semantic,
271
+ content_payload: { fact: 'legacy' },
272
+ domain_tags: ['json']
273
+ )
274
+ store.store(trace)
275
+ store.save_to_local
276
+
277
+ db = Legion::Data::Local.connection
278
+ db[:memory_traces].where(trace_id: trace[:trace_id]).update(emotional_valence: '{"valence":0.8}')
279
+
280
+ fresh = Legion::Extensions::Agentic::Memory::Trace::Helpers::Store.new
281
+ restored = fresh.get(trace[:trace_id])
282
+
283
+ expect(restored[:emotional_valence]).to be_within(0.001).of(0.8)
267
284
  end
268
285
 
269
286
  it 'restores associations from the database into a fresh store' do
@@ -34,5 +34,43 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace do
34
34
 
35
35
  expect(described_class.shared_store).to eq(:shared_store)
36
36
  end
37
+
38
+ it 'falls back from Postgres when user lacks INSERT privilege on memory_traces' do
39
+ allow(described_class).to receive(:local_store_available?).and_return(false)
40
+ allow(described_class).to receive(:configured_trace_store).and_return(nil)
41
+ allow(described_class).to receive(:resolve_agent_id).and_return('agent-1')
42
+
43
+ conn = double('connection', adapter_scheme: :postgres)
44
+ allow(conn).to receive(:table_exists?).and_return(true)
45
+ allow(Legion::Data).to receive(:respond_to?).and_call_original
46
+ allow(Legion::Data).to receive(:respond_to?).with(:connection).and_return(true)
47
+ allow(Legion::Data).to receive(:connection).and_return(conn)
48
+ allow(Legion::Data).to receive(:can_write?).with(:memory_traces).and_return(false)
49
+ allow(Legion::Data).to receive(:can_write?).with(:memory_associations).and_return(true)
50
+
51
+ allow(Legion::Cache).to receive(:respond_to?).with(:connected?).and_return(false)
52
+ allow(described_class::Helpers::Store).to receive(:new).with(partition_id: 'agent-1').and_return(:fallback_store)
53
+
54
+ expect(described_class.shared_store).to eq(:fallback_store)
55
+ end
56
+
57
+ it 'falls back from Postgres when user lacks INSERT privilege on memory_associations' do
58
+ allow(described_class).to receive(:local_store_available?).and_return(false)
59
+ allow(described_class).to receive(:configured_trace_store).and_return(nil)
60
+ allow(described_class).to receive(:resolve_agent_id).and_return('agent-1')
61
+
62
+ conn = double('connection', adapter_scheme: :postgres)
63
+ allow(conn).to receive(:table_exists?).and_return(true)
64
+ allow(Legion::Data).to receive(:respond_to?).and_call_original
65
+ allow(Legion::Data).to receive(:respond_to?).with(:connection).and_return(true)
66
+ allow(Legion::Data).to receive(:connection).and_return(conn)
67
+ allow(Legion::Data).to receive(:can_write?).with(:memory_traces).and_return(true)
68
+ allow(Legion::Data).to receive(:can_write?).with(:memory_associations).and_return(false)
69
+
70
+ allow(Legion::Cache).to receive(:respond_to?).with(:connected?).and_return(false)
71
+ allow(described_class::Helpers::Store).to receive(:new).with(partition_id: 'agent-1').and_return(:fallback_store)
72
+
73
+ expect(described_class.shared_store).to eq(:fallback_store)
74
+ end
37
75
  end
38
76
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-agentic-memory
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.34
4
+ version: 0.1.36
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity