lex-agentic-memory 0.1.32 → 0.1.33
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 +6 -0
- data/lib/legion/extensions/agentic/memory/trace/helpers/postgres_store.rb +6 -3
- data/lib/legion/extensions/agentic/memory/trace/helpers/store.rb +2 -0
- data/lib/legion/extensions/agentic/memory/version.rb +1 -1
- data/spec/legion/extensions/agentic/memory/trace/helpers/postgres_store_spec.rb +57 -0
- data/spec/legion/extensions/agentic/memory/trace/helpers/store_spec.rb +74 -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: 01630a8c5fd981fb85c69337d299f71dc215b79b9ce3dca7a9b0aef57545fde3
|
|
4
|
+
data.tar.gz: 6eb6bd8f8e924aacb9a805561f9f39c0ac18b5d1f559faa7f58a1af3a6d3ad8a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69c12558b2cb6d7a883a9f0bd345ad0934273847b1a44f2dcaed02682a347d9a88dfddbbf6b2ab48d91a418c83f137eb585723dc3aecf19c0a2fb2e1454e7bd3
|
|
7
|
+
data.tar.gz: 8200e4e37b770d4afa344153cd6dc95b9bd047dd55d34079b1bfc6c87c7f261e0049ee2c6f77e4f2b04bf7455745f88eb06160ad006eb81dda7ee0d2053093f9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.33] - 2026-05-07
|
|
4
|
+
### Fixed
|
|
5
|
+
- `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
|
|
6
|
+
- `PostgresStore#parse_json_array` now guards against nil/empty strings before parsing
|
|
7
|
+
- `Store#parse_db_json` now short-circuits on nil/empty columns (domain_tags, associations, child_ids, emotional_valence) before attempting parse, fixing ERROR spam on traces with sparse optional fields
|
|
8
|
+
|
|
3
9
|
## [0.1.32] - 2026-05-07
|
|
4
10
|
### Fixed
|
|
5
11
|
- Trace association retrieval now snapshots associated traces under the store mutex before filtering.
|
|
@@ -394,15 +394,18 @@ module Legion
|
|
|
394
394
|
def parse_json_or_raw(raw)
|
|
395
395
|
return raw unless raw.is_a?(String)
|
|
396
396
|
|
|
397
|
-
|
|
398
|
-
|
|
397
|
+
stripped = raw.strip
|
|
398
|
+
return raw unless stripped.start_with?('{', '[')
|
|
399
|
+
|
|
400
|
+
parsed = Legion::JSON.load(stripped)
|
|
401
|
+
parsed.is_a?(Hash) || parsed.is_a?(Array) ? parsed : raw
|
|
399
402
|
rescue StandardError => e
|
|
400
403
|
log.error "[trace_persistence] parse_json_or_raw: #{e.message}"
|
|
401
404
|
raw
|
|
402
405
|
end
|
|
403
406
|
|
|
404
407
|
def parse_json_array(raw)
|
|
405
|
-
return []
|
|
408
|
+
return [] if raw.nil? || !raw.is_a?(String) || raw.strip.empty?
|
|
406
409
|
|
|
407
410
|
result = Legion::JSON.load(raw)
|
|
408
411
|
result.is_a?(Array) ? result : []
|
|
@@ -358,6 +358,8 @@ module Legion
|
|
|
358
358
|
end
|
|
359
359
|
|
|
360
360
|
def parse_db_json(raw, field, symbolize: false, &default)
|
|
361
|
+
return default&.call if raw.nil? || raw.to_s.strip.empty?
|
|
362
|
+
|
|
361
363
|
parsed = Legion::JSON.load(raw.to_s)
|
|
362
364
|
symbolize ? symbolize_keys(parsed) : parsed
|
|
363
365
|
rescue StandardError => e
|
|
@@ -556,4 +556,61 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::PostgresStor
|
|
|
556
556
|
expect(store.flush).to be_nil
|
|
557
557
|
end
|
|
558
558
|
end
|
|
559
|
+
|
|
560
|
+
# --- plain-text content round-trip (log spam regression) ---
|
|
561
|
+
|
|
562
|
+
describe 'plain-text content deserialization' do
|
|
563
|
+
it 'returns plain-text content as-is without logging errors' do
|
|
564
|
+
trace = trace_helper.new_trace(type: :episodic, content_payload: 'It appears the service is down.')
|
|
565
|
+
store.store(trace)
|
|
566
|
+
|
|
567
|
+
expect(store).not_to receive(:log)
|
|
568
|
+
result = store.retrieve(trace[:trace_id])
|
|
569
|
+
expect(result[:content]).to eq('It appears the service is down.')
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
it 'parses JSON object content into a hash' do
|
|
573
|
+
trace = trace_helper.new_trace(type: :semantic, content_payload: { fact: 'ruby' })
|
|
574
|
+
store.store(trace)
|
|
575
|
+
|
|
576
|
+
result = store.retrieve(trace[:trace_id])
|
|
577
|
+
expect(result[:content]).to be_a(Hash)
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
it 'does not log errors when domain_tags column is nil' do
|
|
581
|
+
trace = trace_helper.new_trace(type: :episodic, content_payload: 'hello')
|
|
582
|
+
store.store(trace)
|
|
583
|
+
|
|
584
|
+
db[:memory_traces].where(trace_id: trace[:trace_id]).update(domain_tags: nil)
|
|
585
|
+
|
|
586
|
+
expect(store).not_to receive(:log)
|
|
587
|
+
result = store.retrieve(trace[:trace_id])
|
|
588
|
+
expect(result[:domain_tags]).to eq([])
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
it 'does not log errors when associations column is nil' do
|
|
592
|
+
trace = trace_helper.new_trace(type: :episodic, content_payload: 'hello')
|
|
593
|
+
store.store(trace)
|
|
594
|
+
|
|
595
|
+
db[:memory_traces].where(trace_id: trace[:trace_id]).update(associations: nil)
|
|
596
|
+
|
|
597
|
+
expect(store).not_to receive(:log)
|
|
598
|
+
result = store.retrieve(trace[:trace_id])
|
|
599
|
+
expect(result[:associated_traces]).to eq([])
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
it 'generates no ERROR log lines during a bulk read with mixed content types' do
|
|
603
|
+
plain_trace = trace_helper.new_trace(type: :episodic, content_payload: 'Hello, I am plain text')
|
|
604
|
+
json_trace = trace_helper.new_trace(type: :semantic, content_payload: { fact: 'structured' })
|
|
605
|
+
store.store(plain_trace)
|
|
606
|
+
store.store(json_trace)
|
|
607
|
+
|
|
608
|
+
log_double = double('log', debug: nil, info: nil, warn: nil)
|
|
609
|
+
allow(store).to receive(:log).and_return(log_double)
|
|
610
|
+
expect(log_double).not_to receive(:error)
|
|
611
|
+
|
|
612
|
+
results = store.all_traces
|
|
613
|
+
expect(results.size).to eq(2)
|
|
614
|
+
end
|
|
615
|
+
end
|
|
559
616
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'sequel'
|
|
4
|
+
|
|
3
5
|
RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::Store do
|
|
4
6
|
let(:store) { described_class.new }
|
|
5
7
|
let(:trace_helper) { Legion::Extensions::Agentic::Memory::Trace::Helpers::Trace }
|
|
@@ -235,6 +237,78 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::Store do
|
|
|
235
237
|
end
|
|
236
238
|
end
|
|
237
239
|
|
|
240
|
+
describe 'parse_db_json nil/empty guard (log spam regression)' do
|
|
241
|
+
let(:local_db) do
|
|
242
|
+
d = Sequel.sqlite
|
|
243
|
+
d.create_table(:memory_traces) do
|
|
244
|
+
primary_key :id
|
|
245
|
+
String :trace_id, size: 36, null: false, unique: true
|
|
246
|
+
String :trace_type, null: false, default: 'episodic'
|
|
247
|
+
String :content, text: true, null: false, default: ''
|
|
248
|
+
Float :strength, default: 1.0
|
|
249
|
+
Float :peak_strength, default: 1.0
|
|
250
|
+
Float :base_decay_rate, default: 0.02
|
|
251
|
+
Float :emotional_valence
|
|
252
|
+
Float :emotional_intensity, default: 0.0
|
|
253
|
+
String :domain_tags, text: true
|
|
254
|
+
String :origin
|
|
255
|
+
String :storage_tier, default: 'hot'
|
|
256
|
+
DateTime :created_at
|
|
257
|
+
DateTime :last_reinforced
|
|
258
|
+
DateTime :last_decayed
|
|
259
|
+
Integer :reinforcement_count, default: 0
|
|
260
|
+
Float :confidence, default: 0.5
|
|
261
|
+
String :partition_id
|
|
262
|
+
String :associated_traces, text: true
|
|
263
|
+
String :parent_id
|
|
264
|
+
String :child_ids, text: true
|
|
265
|
+
TrueClass :unresolved, default: false
|
|
266
|
+
TrueClass :consolidation_candidate, default: false
|
|
267
|
+
end
|
|
268
|
+
d.create_table(:memory_associations) do
|
|
269
|
+
primary_key :id
|
|
270
|
+
String :trace_id_a, size: 36, null: false
|
|
271
|
+
String :trace_id_b, size: 36, null: false
|
|
272
|
+
Integer :coactivation_count, default: 1, null: false
|
|
273
|
+
String :partition_id
|
|
274
|
+
unique %i[trace_id_a trace_id_b]
|
|
275
|
+
end
|
|
276
|
+
d
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
let(:data_local_stub) do
|
|
280
|
+
conn = local_db
|
|
281
|
+
Module.new do
|
|
282
|
+
define_singleton_method(:connected?) { true }
|
|
283
|
+
define_singleton_method(:connection) { conn }
|
|
284
|
+
define_singleton_method(:table_exists?) { |name| conn.table_exists?(name) }
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
before { stub_const('Legion::Data::Local', data_local_stub) }
|
|
289
|
+
|
|
290
|
+
it 'does not log errors when nil optional columns are deserialized on load' do
|
|
291
|
+
local_db[:memory_traces].insert(
|
|
292
|
+
trace_id: SecureRandom.uuid,
|
|
293
|
+
trace_type: 'episodic',
|
|
294
|
+
content: 'plain text content',
|
|
295
|
+
partition_id: 'default',
|
|
296
|
+
strength: 1.0,
|
|
297
|
+
domain_tags: nil,
|
|
298
|
+
associated_traces: nil,
|
|
299
|
+
child_ids: nil,
|
|
300
|
+
emotional_valence: nil
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
fresh = described_class.new
|
|
304
|
+
expect(fresh.count).to eq(1)
|
|
305
|
+
trace = fresh.traces.values.first
|
|
306
|
+
expect(trace[:domain_tags]).to eq([])
|
|
307
|
+
expect(trace[:associated_traces]).to eq([])
|
|
308
|
+
expect(trace[:child_trace_ids]).to eq([])
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
238
312
|
describe '#restore_traces' do
|
|
239
313
|
it 'replaces existing traces and clears stale associations' do
|
|
240
314
|
store.store(semantic_trace)
|