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 +4 -4
- data/CHANGELOG.md +10 -0
- data/lib/legion/extensions/agentic/memory/archaeology/helpers/archaeology_engine.rb +3 -3
- data/lib/legion/extensions/agentic/memory/trace/helpers/cache_store.rb +4 -3
- data/lib/legion/extensions/agentic/memory/trace/helpers/postgres_store.rb +11 -8
- data/lib/legion/extensions/agentic/memory/trace/helpers/store.rb +10 -4
- data/lib/legion/extensions/agentic/memory/trace/helpers/trace.rb +86 -2
- data/lib/legion/extensions/agentic/memory/trace.rb +3 -1
- data/lib/legion/extensions/agentic/memory/version.rb +1 -1
- data/spec/legion/extensions/agentic/memory/hologram/helpers/hologram_engine_spec.rb +3 -0
- data/spec/legion/extensions/agentic/memory/trace/helpers/postgres_store_spec.rb +38 -0
- data/spec/legion/extensions/agentic/memory/trace/helpers/trace_spec.rb +24 -0
- data/spec/legion/extensions/agentic/memory/trace/local_persistence_spec.rb +21 -4
- data/spec/legion/extensions/agentic/memory/trace/memory_spec.rb +38 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f135722a81c15e36374e61e6348db1f421db9b4fc5257bc2915d8526a9b7a145
|
|
4
|
+
data.tar.gz: 86dfb884036c9d7d8617a3a785b6b35c6cc0db413b60a9866aad04ad3e899d59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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[
|
|
43
|
-
@dirty_ids <<
|
|
43
|
+
@traces[normalized_trace[:trace_id]] = normalized_trace
|
|
44
|
+
@dirty_ids << normalized_trace[:trace_id]
|
|
44
45
|
end
|
|
45
|
-
|
|
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
|
-
|
|
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(
|
|
38
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
72
|
-
emotional_intensity: emotional_intensity
|
|
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
|
|
@@ -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 '
|
|
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:
|
|
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
|
|
266
|
-
|
|
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
|