lex-agentic-memory 0.1.32 → 0.1.34

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: 07b491ea4b919f623905cf3fe0c651c6a462e46bfe096e06e4221e9eb9e6b801
4
- data.tar.gz: b92fc5808525806e2dca50c86ea4382583adcbe5b6b9e84d0dd8e1c4add0f14b
3
+ metadata.gz: 606b711547732f1f697b57ce2392509c2774397c725b51f87baeaee591cf2ae2
4
+ data.tar.gz: f9f704beca9f4f33c8d71ad54627ef400561819898aeebf06c0097a0c0a535b1
5
5
  SHA512:
6
- metadata.gz: ed8eb6cff096080b1435aab249896d4c473d1aab7b11ebec1f687081aa9494bc8671c029d3d7e2e80b4144b705b477e6d58fc796c412d2a6bf75a852fa02f27e
7
- data.tar.gz: 834d02319dbf0c63722d8bc8e59e5f63d6c18c42c740a71a47ac5f1c6df0c1ad9b983e73b246f751bd24a784aa4e43c5d5f508ed516dbd7f94321fea220f2cf3
6
+ metadata.gz: 35f855a01efbfb5b29f23fe12f92e89082a3a4c8001b149dd16f6ccd648bc9d67a699755230e5b08c4a86f1147522f86b71d5fea3e31f3842448c20e4af3d094
7
+ data.tar.gz: 8e9afdf79c62437fefe856a61a0432d1574a7fba7dadab51c4a6b19a0e8a6d6a3658a19fbfa725d5cdb5c4c8de300169818aeee15c1e204e9cf7ff15a9cf12d3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
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.33] - 2026-05-07
9
+ ### Fixed
10
+ - `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
11
+ - `PostgresStore#parse_json_array` now guards against nil/empty strings before parsing
12
+ - `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
13
+
3
14
  ## [0.1.32] - 2026-05-07
4
15
  ### Fixed
5
16
  - Trace association retrieval now snapshots associated traces under the store mutex before filtering.
@@ -18,12 +18,14 @@ module Legion
18
18
  attr_reader :store
19
19
 
20
20
  def initialize(store: nil, **)
21
- @default_store = store || Legion::Extensions::Agentic::Memory::Trace.shared_store
21
+ @default_store = store if store
22
22
  end
23
23
 
24
24
  private
25
25
 
26
- attr_reader :default_store
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
@@ -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
- parsed = Legion::JSON.load(raw)
398
- parsed.is_a?(Hash) ? parsed : raw
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 [] unless raw.is_a?(String)
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 : []
@@ -348,16 +348,25 @@ module Legion
348
348
  return raw unless raw.is_a?(String)
349
349
 
350
350
  stripped = raw.strip
351
- return raw unless stripped.start_with?('{', '[')
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 => e
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
+
360
367
  def parse_db_json(raw, field, symbolize: false, &default)
368
+ return default&.call if raw.nil? || raw.to_s.strip.empty?
369
+
361
370
  parsed = Legion::JSON.load(raw.to_s)
362
371
  symbolize ? symbolize_keys(parsed) : parsed
363
372
  rescue StandardError => e
@@ -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
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Agentic
6
6
  module Memory
7
- VERSION = '0.1.32'
7
+ VERSION = '0.1.34'
8
8
  end
9
9
  end
10
10
  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)
@@ -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 }
@@ -126,6 +128,30 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::Store do
126
128
  end
127
129
  end
128
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
+
129
155
  describe '#count' do
130
156
  it 'returns number of stored traces' do
131
157
  expect(store.count).to eq(0)
@@ -235,6 +261,78 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::Store do
235
261
  end
236
262
  end
237
263
 
264
+ describe 'parse_db_json nil/empty guard (log spam regression)' do
265
+ let(:local_db) do
266
+ d = Sequel.sqlite
267
+ d.create_table(:memory_traces) do
268
+ primary_key :id
269
+ String :trace_id, size: 36, null: false, unique: true
270
+ String :trace_type, null: false, default: 'episodic'
271
+ String :content, text: true, null: false, default: ''
272
+ Float :strength, default: 1.0
273
+ Float :peak_strength, default: 1.0
274
+ Float :base_decay_rate, default: 0.02
275
+ Float :emotional_valence
276
+ Float :emotional_intensity, default: 0.0
277
+ String :domain_tags, text: true
278
+ String :origin
279
+ String :storage_tier, default: 'hot'
280
+ DateTime :created_at
281
+ DateTime :last_reinforced
282
+ DateTime :last_decayed
283
+ Integer :reinforcement_count, default: 0
284
+ Float :confidence, default: 0.5
285
+ String :partition_id
286
+ String :associated_traces, text: true
287
+ String :parent_id
288
+ String :child_ids, text: true
289
+ TrueClass :unresolved, default: false
290
+ TrueClass :consolidation_candidate, default: false
291
+ end
292
+ d.create_table(:memory_associations) do
293
+ primary_key :id
294
+ String :trace_id_a, size: 36, null: false
295
+ String :trace_id_b, size: 36, null: false
296
+ Integer :coactivation_count, default: 1, null: false
297
+ String :partition_id
298
+ unique %i[trace_id_a trace_id_b]
299
+ end
300
+ d
301
+ end
302
+
303
+ let(:data_local_stub) do
304
+ conn = local_db
305
+ Module.new do
306
+ define_singleton_method(:connected?) { true }
307
+ define_singleton_method(:connection) { conn }
308
+ define_singleton_method(:table_exists?) { |name| conn.table_exists?(name) }
309
+ end
310
+ end
311
+
312
+ before { stub_const('Legion::Data::Local', data_local_stub) }
313
+
314
+ it 'does not log errors when nil optional columns are deserialized on load' do
315
+ local_db[:memory_traces].insert(
316
+ trace_id: SecureRandom.uuid,
317
+ trace_type: 'episodic',
318
+ content: 'plain text content',
319
+ partition_id: 'default',
320
+ strength: 1.0,
321
+ domain_tags: nil,
322
+ associated_traces: nil,
323
+ child_ids: nil,
324
+ emotional_valence: nil
325
+ )
326
+
327
+ fresh = described_class.new
328
+ expect(fresh.count).to eq(1)
329
+ trace = fresh.traces.values.first
330
+ expect(trace[:domain_tags]).to eq([])
331
+ expect(trace[:associated_traces]).to eq([])
332
+ expect(trace[:child_trace_ids]).to eq([])
333
+ end
334
+ end
335
+
238
336
  describe '#restore_traces' do
239
337
  it 'replaces existing traces and clears stale associations' do
240
338
  store.store(semantic_trace)
@@ -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
 
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.32
4
+ version: 0.1.34
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity