lex-agentic-memory 0.1.33 → 0.1.35
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/client.rb +4 -2
- data/lib/legion/extensions/agentic/memory/trace/helpers/cache_store.rb +4 -3
- data/lib/legion/extensions/agentic/memory/trace/helpers/postgres_store.rb +7 -6
- data/lib/legion/extensions/agentic/memory/trace/helpers/store.rb +20 -7
- data/lib/legion/extensions/agentic/memory/trace/helpers/trace.rb +86 -2
- data/lib/legion/extensions/agentic/memory/trace/runners/consolidation.rb +7 -3
- data/lib/legion/extensions/agentic/memory/version.rb +1 -1
- data/spec/legion/extensions/agentic/memory/trace/client_spec.rb +8 -0
- data/spec/legion/extensions/agentic/memory/trace/helpers/postgres_store_spec.rb +14 -0
- data/spec/legion/extensions/agentic/memory/trace/helpers/store_spec.rb +24 -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/runners/consolidation_spec.rb +17 -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: bb940d62021d4a86ed5de171d6b275c9287a9ca727efadd3c60c3a87ca0f921e
|
|
4
|
+
data.tar.gz: b02842d4a5939e436d7ae4f72cca295839061f5fa4cdbf9b47736f37e462e76e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e28e286f59952562c8c00ad37cb06f84914f2b93af864cb95a1d9904c218dc72d28bb09a4df049c418f5a66df752a199171c0e553e684c234ae888ab73be24a8
|
|
7
|
+
data.tar.gz: 12febef87343cde5c4ca8f372c62a6f7d5b053a47ad5362710ee54224be60ceb899f7f0f32eeb65cc9474ebe5b47535731e2f6a3e83a89f3d315c6780774ea77
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.34] - 2026-05-08
|
|
4
|
+
### Fixed
|
|
5
|
+
- Deferred GAIA heartbeat maintenance no longer initializes the shared trace store just to return a deferred summary, avoiding idle local trace table scans.
|
|
6
|
+
- `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
|
+
|
|
8
|
+
## [0.1.35] - 2026-05-15
|
|
9
|
+
### Fixed
|
|
10
|
+
- 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`.
|
|
11
|
+
- Preserve numeric `emotional_valence` across local trace persistence and normalize legacy structured payloads when they are reloaded.
|
|
12
|
+
|
|
3
13
|
## [0.1.33] - 2026-05-07
|
|
4
14
|
### Fixed
|
|
5
15
|
- `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
|
|
|
@@ -18,12 +18,14 @@ module Legion
|
|
|
18
18
|
attr_reader :store
|
|
19
19
|
|
|
20
20
|
def initialize(store: nil, **)
|
|
21
|
-
@default_store = store
|
|
21
|
+
@default_store = store if store
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
def default_store
|
|
27
|
+
@default_store ||= Legion::Extensions::Agentic::Memory::Trace.shared_store
|
|
28
|
+
end
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
end
|
|
@@ -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
|
|
@@ -299,7 +300,7 @@ module Legion
|
|
|
299
300
|
tags = trace[:domain_tags]
|
|
300
301
|
assocs = trace[:associated_traces]
|
|
301
302
|
conf = trace[:confidence]
|
|
302
|
-
ev = trace[:emotional_valence]
|
|
303
|
+
ev = Helpers::Trace.normalize_emotional_valence(trace[:emotional_valence])
|
|
303
304
|
|
|
304
305
|
{
|
|
305
306
|
trace_id: trace[:trace_id],
|
|
@@ -314,8 +315,8 @@ module Legion
|
|
|
314
315
|
strength: trace[:strength],
|
|
315
316
|
peak_strength: trace[:peak_strength],
|
|
316
317
|
base_decay_rate: trace[:base_decay_rate],
|
|
317
|
-
emotional_valence: ev
|
|
318
|
-
emotional_intensity: trace[:emotional_intensity],
|
|
318
|
+
emotional_valence: ev,
|
|
319
|
+
emotional_intensity: Helpers::Trace.normalize_emotional_intensity(trace[:emotional_intensity]),
|
|
319
320
|
origin: sanitize_pg_string(trace[:origin].to_s),
|
|
320
321
|
source_agent_id: sanitize_pg_string(trace[:source_agent_id]),
|
|
321
322
|
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,
|
|
@@ -348,15 +348,28 @@ module Legion
|
|
|
348
348
|
return raw unless raw.is_a?(String)
|
|
349
349
|
|
|
350
350
|
stripped = raw.strip
|
|
351
|
-
return raw unless
|
|
351
|
+
return raw unless parseable_content_json?(stripped)
|
|
352
352
|
|
|
353
353
|
parsed = Legion::JSON.load(stripped)
|
|
354
354
|
parsed.is_a?(Hash) || parsed.is_a?(Array) ? parsed : raw
|
|
355
|
-
rescue StandardError =>
|
|
356
|
-
log.debug "[trace_persistence] malformed JSON in content column, returning raw: #{e.message}"
|
|
355
|
+
rescue StandardError => _e
|
|
357
356
|
raw
|
|
358
357
|
end
|
|
359
358
|
|
|
359
|
+
def parseable_content_json?(value)
|
|
360
|
+
return true if value.start_with?('{')
|
|
361
|
+
return false unless value.start_with?('[')
|
|
362
|
+
|
|
363
|
+
first_array_value = value[1..]&.lstrip&.[](0)
|
|
364
|
+
%w[{ [ " ]].include?(first_array_value)
|
|
365
|
+
end
|
|
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
|
+
|
|
360
373
|
def parse_db_json(raw, field, symbolize: false, &default)
|
|
361
374
|
return default&.call if raw.nil? || raw.to_s.strip.empty?
|
|
362
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
|
|
@@ -31,12 +31,12 @@ module Legion
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def decay_cycle(store: nil, tick_count: 1, maintenance: true, **)
|
|
34
|
-
store ||= default_store
|
|
35
34
|
unless maintenance
|
|
36
|
-
deferred = deferred_decay_summary(store)
|
|
35
|
+
deferred = deferred_decay_summary(store || cached_default_store)
|
|
37
36
|
return { **deferred }
|
|
38
37
|
end
|
|
39
38
|
|
|
39
|
+
store ||= default_store
|
|
40
40
|
decayed = 0
|
|
41
41
|
pruned = 0
|
|
42
42
|
total = trace_count(store)
|
|
@@ -139,7 +139,7 @@ module Legion
|
|
|
139
139
|
|
|
140
140
|
def deferred_decay_summary(store)
|
|
141
141
|
summary = Legion::Extensions::Agentic::Memory::Trace.last_maintenance_summary || {}
|
|
142
|
-
current_count = trace_count(store)
|
|
142
|
+
current_count = store ? trace_count(store) : 0
|
|
143
143
|
|
|
144
144
|
{
|
|
145
145
|
decayed: summary[:decayed] || 0,
|
|
@@ -173,6 +173,10 @@ module Legion
|
|
|
173
173
|
@default_store ||= Legion::Extensions::Agentic::Memory::Trace.shared_store
|
|
174
174
|
end
|
|
175
175
|
|
|
176
|
+
def cached_default_store
|
|
177
|
+
@default_store if instance_variable_defined?(:@default_store)
|
|
178
|
+
end
|
|
179
|
+
|
|
176
180
|
include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
|
|
177
181
|
end
|
|
178
182
|
end
|
|
@@ -24,6 +24,14 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Client do
|
|
|
24
24
|
expect(client).to respond_to(:erase_by_agent)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
it 'does not initialize the shared trace store until a store-backed operation needs it' do
|
|
28
|
+
allow(Legion::Extensions::Agentic::Memory::Trace).to receive(:shared_store)
|
|
29
|
+
|
|
30
|
+
described_class.new
|
|
31
|
+
|
|
32
|
+
expect(Legion::Extensions::Agentic::Memory::Trace).not_to have_received(:shared_store)
|
|
33
|
+
end
|
|
34
|
+
|
|
27
35
|
it 'uses provided store' do
|
|
28
36
|
store = Legion::Extensions::Agentic::Memory::Trace::Helpers::Store.new
|
|
29
37
|
client = described_class.new(store: store)
|
|
@@ -238,6 +238,20 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::PostgresStor
|
|
|
238
238
|
end
|
|
239
239
|
end
|
|
240
240
|
|
|
241
|
+
describe 'emotional valence normalization' do
|
|
242
|
+
it 'normalizes string-backed affect fields before persisting' do
|
|
243
|
+
trace = trace_helper.new_trace(type: :episodic, content_payload: { event: 'partner ping' })
|
|
244
|
+
trace[:emotional_valence] = '0.7'
|
|
245
|
+
trace[:emotional_intensity] = '0.9'
|
|
246
|
+
|
|
247
|
+
store.store(trace)
|
|
248
|
+
|
|
249
|
+
row = db[:memory_traces].where(trace_id: trace[:trace_id]).first
|
|
250
|
+
expect(row[:emotional_valence]).to be_within(0.001).of(0.7)
|
|
251
|
+
expect(row[:emotional_intensity]).to be_within(0.001).of(0.9)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
241
255
|
# --- retrieve_by_type ---
|
|
242
256
|
|
|
243
257
|
describe '#retrieve_by_type' do
|
|
@@ -128,6 +128,30 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::Store do
|
|
|
128
128
|
end
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
+
describe '#parse_db_content' do
|
|
132
|
+
let(:parser) { described_class.allocate }
|
|
133
|
+
|
|
134
|
+
it 'returns bracket-prefixed raw log text without logging malformed JSON noise' do
|
|
135
|
+
expect(parser).not_to receive(:log)
|
|
136
|
+
|
|
137
|
+
result = parser.send(:parse_db_content, '[tool][file_read] opened file')
|
|
138
|
+
|
|
139
|
+
expect(result).to eq('[tool][file_read] opened file')
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'still parses JSON object content' do
|
|
143
|
+
result = parser.send(:parse_db_content, '{"event":"meeting"}')
|
|
144
|
+
|
|
145
|
+
expect(result).to eq({ event: 'meeting' })
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'still parses JSON array content when it looks like serialized payload data' do
|
|
149
|
+
result = parser.send(:parse_db_content, '[{"event":"meeting"}]')
|
|
150
|
+
|
|
151
|
+
expect(result).to eq([{ event: 'meeting' }])
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
131
155
|
describe '#count' do
|
|
132
156
|
it 'returns number of stored traces' do
|
|
133
157
|
expect(store.count).to eq(0)
|
|
@@ -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
|
|
@@ -53,6 +53,23 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Runners::Consolidatio
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
describe '#decay_cycle' do
|
|
56
|
+
it 'does not initialize the shared store when Gaia defers heartbeat maintenance' do
|
|
57
|
+
runner = Object.new.extend(described_class)
|
|
58
|
+
allow(Legion::Extensions::Agentic::Memory::Trace).to receive(:shared_store)
|
|
59
|
+
|
|
60
|
+
result = runner.decay_cycle(maintenance: false)
|
|
61
|
+
|
|
62
|
+
expect(Legion::Extensions::Agentic::Memory::Trace).not_to have_received(:shared_store)
|
|
63
|
+
expect(result).to include(
|
|
64
|
+
decayed: 0,
|
|
65
|
+
pruned: 0,
|
|
66
|
+
total: 0,
|
|
67
|
+
remaining: 0,
|
|
68
|
+
deferred: true,
|
|
69
|
+
reason: :background_decay_actor
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
56
73
|
it 'defers Gaia heartbeat decay work to the background actor when maintenance is false' do
|
|
57
74
|
client.store_trace(type: :semantic, content_payload: {})
|
|
58
75
|
|