lex-agentic-memory 0.1.20 → 0.1.21
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/Gemfile +2 -0
- data/lex-agentic-memory.gemspec +3 -3
- data/lib/legion/extensions/agentic/memory/trace/helpers/error_tracer.rb +26 -16
- data/lib/legion/extensions/agentic/memory/version.rb +1 -1
- data/spec/legion/extensions/agentic/memory/trace/helpers/error_tracer_spec.rb +128 -0
- metadata +13 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f403d34cba75eaf3f800a8852c8d6e16d2f56924b2454b7c444a0cee82a00f04
|
|
4
|
+
data.tar.gz: f946558e6cf3103bc32bf44d9accb9a4c3a13d91342e7a06a5f6b5ec98c0c856
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e49ff76e259d4237f19a7998f8e973438ebcfceb44322990a6213f35bd07481adfea5f8cd037c351217af569931f2344ceb04b5603bc53cadd5c27d02cec4cbe
|
|
7
|
+
data.tar.gz: 28a6d1d21d23f0ba91d034b441ee7e858fc51d1d1662e9445b58ac215e09dd271871f90941121d04566e259d971cf6d615c56412ccf2cd9f08e5658e1dd20f0f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.21] - 2026-04-01
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- `ErrorTracer.record_trace` now dispatches `Trace.shared_store` writes/flushes in a background `Thread.new` — error/fatal logging hooks no longer block the calling Puma thread, regardless of store backend (Postgres, CacheStore, or in-memory)
|
|
7
|
+
- Debounce check remains synchronous; only the store write and flush go async
|
|
8
|
+
|
|
3
9
|
## [0.1.20] - 2026-03-31
|
|
4
10
|
|
|
5
11
|
### Added
|
data/Gemfile
CHANGED
data/lex-agentic-memory.gemspec
CHANGED
|
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
|
|
|
34
34
|
spec.add_dependency 'msgpack', '~> 1.7'
|
|
35
35
|
|
|
36
36
|
spec.add_development_dependency 'rspec', '~> 3.13'
|
|
37
|
-
spec.add_development_dependency 'rubocop'
|
|
38
|
-
spec.add_development_dependency 'rubocop-legion'
|
|
39
|
-
spec.add_development_dependency 'rubocop-rspec'
|
|
37
|
+
spec.add_development_dependency 'rubocop'
|
|
38
|
+
spec.add_development_dependency 'rubocop-legion'
|
|
39
|
+
spec.add_development_dependency 'rubocop-rspec'
|
|
40
40
|
end
|
|
@@ -17,8 +17,12 @@ module Legion
|
|
|
17
17
|
def setup
|
|
18
18
|
return if @active
|
|
19
19
|
|
|
20
|
-
@recent
|
|
21
|
-
@
|
|
20
|
+
@recent = {}
|
|
21
|
+
@recent_mutex = ::Mutex.new
|
|
22
|
+
@runner = Object.new.extend(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
|
|
23
|
+
@write_queue = ::Queue.new
|
|
24
|
+
@worker = ::Thread.new { drain_queue }
|
|
25
|
+
@worker.name = 'legion-error-tracer'
|
|
22
26
|
wrap_logging_methods
|
|
23
27
|
@active = true
|
|
24
28
|
Legion::Logging.info '[memory] ErrorTracer active — errors/fatals will become episodic traces'
|
|
@@ -30,6 +34,19 @@ module Legion
|
|
|
30
34
|
|
|
31
35
|
private
|
|
32
36
|
|
|
37
|
+
def drain_queue
|
|
38
|
+
loop do
|
|
39
|
+
payload = @write_queue.pop
|
|
40
|
+
break if payload == :stop
|
|
41
|
+
|
|
42
|
+
@runner.store_trace(**payload)
|
|
43
|
+
store = @runner.send(:default_store)
|
|
44
|
+
store.flush if store.respond_to?(:flush)
|
|
45
|
+
rescue StandardError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
33
50
|
def wrap_logging_methods
|
|
34
51
|
original_error = Legion::Logging.method(:error)
|
|
35
52
|
original_fatal = Legion::Logging.method(:fatal)
|
|
@@ -50,23 +67,21 @@ module Legion
|
|
|
50
67
|
def record_trace(message, level)
|
|
51
68
|
return unless message.is_a?(String) && !message.empty?
|
|
52
69
|
|
|
53
|
-
# Debounce: skip if same message within window
|
|
54
70
|
now = Time.now.utc
|
|
55
71
|
key = "#{level}:#{message[0..100]}"
|
|
56
|
-
return if @recent[key] && (now - @recent[key]) < DEBOUNCE_WINDOW
|
|
57
72
|
|
|
58
|
-
@
|
|
73
|
+
@recent_mutex.synchronize do
|
|
74
|
+
return if @recent[key] && (now - @recent[key]) < DEBOUNCE_WINDOW
|
|
59
75
|
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
@recent[key] = now
|
|
77
|
+
@recent.delete_if { |_, t| (now - t) > DEBOUNCE_WINDOW } if @recent.size > 500
|
|
78
|
+
end
|
|
62
79
|
|
|
63
|
-
# Extract component from [bracket] prefix
|
|
64
80
|
component = message.match(/\A\[([^\]]+)\]/)&.captures&.first || 'unknown'
|
|
65
|
-
|
|
66
81
|
valence = level == :fatal ? FATAL_VALENCE : ERROR_VALENCE
|
|
67
82
|
intensity = level == :fatal ? FATAL_INTENSITY : ERROR_INTENSITY
|
|
68
83
|
|
|
69
|
-
@
|
|
84
|
+
@write_queue.push(
|
|
70
85
|
type: :episodic,
|
|
71
86
|
content_payload: message,
|
|
72
87
|
domain_tags: ['error', component.downcase],
|
|
@@ -76,12 +91,7 @@ module Legion
|
|
|
76
91
|
unresolved: true,
|
|
77
92
|
confidence: 0.9
|
|
78
93
|
)
|
|
79
|
-
|
|
80
|
-
# Flush if cache-backed
|
|
81
|
-
store = @runner.send(:default_store)
|
|
82
|
-
store.flush if store.respond_to?(:flush)
|
|
83
|
-
rescue StandardError => _e
|
|
84
|
-
# Never let trace creation break the logging pipeline
|
|
94
|
+
rescue StandardError
|
|
85
95
|
nil
|
|
86
96
|
end
|
|
87
97
|
end
|
|
@@ -11,13 +11,24 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::ErrorTracer
|
|
|
11
11
|
# Reset state between examples
|
|
12
12
|
described_class.instance_variable_set(:@active, nil)
|
|
13
13
|
described_class.instance_variable_set(:@recent, nil)
|
|
14
|
+
described_class.instance_variable_set(:@recent_mutex, nil)
|
|
14
15
|
described_class.instance_variable_set(:@runner, nil)
|
|
16
|
+
described_class.instance_variable_set(:@write_queue, nil)
|
|
17
|
+
old_worker = described_class.instance_variable_get(:@worker)
|
|
18
|
+
old_worker&.kill
|
|
19
|
+
described_class.instance_variable_set(:@worker, nil)
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
after do
|
|
18
23
|
# Restore logging singleton methods to prevent cross-test side effects
|
|
19
24
|
Legion::Logging.define_singleton_method(:error, &original_error)
|
|
20
25
|
Legion::Logging.define_singleton_method(:fatal, &original_fatal)
|
|
26
|
+
# Stop worker thread if still alive
|
|
27
|
+
worker = described_class.instance_variable_get(:@worker)
|
|
28
|
+
if worker&.alive?
|
|
29
|
+
described_class.instance_variable_get(:@write_queue)&.push(:stop)
|
|
30
|
+
worker.join(1)
|
|
31
|
+
end
|
|
21
32
|
end
|
|
22
33
|
|
|
23
34
|
describe '.setup' do
|
|
@@ -31,6 +42,13 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::ErrorTracer
|
|
|
31
42
|
described_class.setup
|
|
32
43
|
expect(described_class.active?).to be true
|
|
33
44
|
end
|
|
45
|
+
|
|
46
|
+
it 'starts a single background worker thread' do
|
|
47
|
+
described_class.setup
|
|
48
|
+
worker = described_class.instance_variable_get(:@worker)
|
|
49
|
+
expect(worker).to be_a(Thread)
|
|
50
|
+
expect(worker).to be_alive
|
|
51
|
+
end
|
|
34
52
|
end
|
|
35
53
|
|
|
36
54
|
describe '.active?' do
|
|
@@ -43,4 +61,114 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::ErrorTracer
|
|
|
43
61
|
expect(described_class.active?).to be true
|
|
44
62
|
end
|
|
45
63
|
end
|
|
64
|
+
|
|
65
|
+
describe 'record_trace (async dispatch via queue)' do
|
|
66
|
+
let(:store_dbl) { double('store', flush: nil) }
|
|
67
|
+
let(:runner_double) do
|
|
68
|
+
dbl = double('runner')
|
|
69
|
+
allow(dbl).to receive(:store_trace)
|
|
70
|
+
allow(dbl).to receive(:default_store).and_return(store_dbl)
|
|
71
|
+
dbl
|
|
72
|
+
end
|
|
73
|
+
let(:write_queue) { Queue.new }
|
|
74
|
+
|
|
75
|
+
before do
|
|
76
|
+
described_class.instance_variable_set(:@active, true)
|
|
77
|
+
described_class.instance_variable_set(:@recent, {})
|
|
78
|
+
described_class.instance_variable_set(:@recent_mutex, Mutex.new)
|
|
79
|
+
described_class.instance_variable_set(:@runner, runner_double)
|
|
80
|
+
described_class.instance_variable_set(:@write_queue, write_queue)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'enqueues a payload hash (fire-and-forget dispatch)' do
|
|
84
|
+
described_class.send(:record_trace, 'something broke', :error)
|
|
85
|
+
expect(write_queue.size).to eq(1)
|
|
86
|
+
payload = write_queue.pop
|
|
87
|
+
expect(payload).to be_a(Hash)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'enqueues a payload with expected keys' do
|
|
91
|
+
described_class.send(:record_trace, '[mycomponent] disk full', :error)
|
|
92
|
+
payload = write_queue.pop
|
|
93
|
+
expect(payload).to include(
|
|
94
|
+
type: :episodic,
|
|
95
|
+
content_payload: '[mycomponent] disk full',
|
|
96
|
+
domain_tags: %w[error mycomponent],
|
|
97
|
+
unresolved: true
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'uses fatal valence and intensity for :fatal level' do
|
|
102
|
+
described_class.send(:record_trace, 'total meltdown', :fatal)
|
|
103
|
+
payload = write_queue.pop
|
|
104
|
+
expect(payload).to include(
|
|
105
|
+
emotional_valence: Legion::Extensions::Agentic::Memory::Trace::Helpers::ErrorTracer::FATAL_VALENCE,
|
|
106
|
+
emotional_intensity: Legion::Extensions::Agentic::Memory::Trace::Helpers::ErrorTracer::FATAL_INTENSITY
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'does not enqueue (debounced) when same message is within the window' do
|
|
111
|
+
described_class.send(:record_trace, 'repeated error', :error)
|
|
112
|
+
described_class.send(:record_trace, 'repeated error', :error)
|
|
113
|
+
expect(write_queue.size).to eq(1)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'returns nil for blank messages without raising' do
|
|
117
|
+
expect(described_class.send(:record_trace, '', :error)).to be_nil
|
|
118
|
+
expect(write_queue.size).to eq(0)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'does not propagate errors from store_trace' do
|
|
122
|
+
worker_queue = Queue.new
|
|
123
|
+
described_class.instance_variable_set(:@write_queue, worker_queue)
|
|
124
|
+
allow(runner_double).to receive(:store_trace).and_raise(StandardError, 'db down')
|
|
125
|
+
|
|
126
|
+
# rubocop:disable ThreadSafety/NewThread
|
|
127
|
+
worker = Thread.new do
|
|
128
|
+
payload = worker_queue.pop
|
|
129
|
+
break if payload == :stop
|
|
130
|
+
|
|
131
|
+
runner_double.store_trace(**payload)
|
|
132
|
+
rescue StandardError
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
# rubocop:enable ThreadSafety/NewThread
|
|
136
|
+
|
|
137
|
+
described_class.send(:record_trace, 'store failure', :error)
|
|
138
|
+
expect { worker.join(2) }.not_to raise_error
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it 'the background worker calls store_trace with the enqueued payload' do
|
|
142
|
+
dedicated_queue = Queue.new
|
|
143
|
+
described_class.instance_variable_set(:@write_queue, dedicated_queue)
|
|
144
|
+
|
|
145
|
+
# rubocop:disable ThreadSafety/NewThread
|
|
146
|
+
worker = Thread.new do
|
|
147
|
+
loop do
|
|
148
|
+
payload = dedicated_queue.pop
|
|
149
|
+
break if payload == :stop
|
|
150
|
+
|
|
151
|
+
runner_double.store_trace(**payload)
|
|
152
|
+
store = runner_double.send(:default_store)
|
|
153
|
+
store.flush if store.respond_to?(:flush)
|
|
154
|
+
rescue StandardError
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
# rubocop:enable ThreadSafety/NewThread
|
|
159
|
+
|
|
160
|
+
described_class.send(:record_trace, '[mycomponent] disk full', :error)
|
|
161
|
+
dedicated_queue.push(:stop)
|
|
162
|
+
worker.join(2)
|
|
163
|
+
|
|
164
|
+
expect(runner_double).to have_received(:store_trace).with(
|
|
165
|
+
hash_including(
|
|
166
|
+
type: :episodic,
|
|
167
|
+
content_payload: '[mycomponent] disk full',
|
|
168
|
+
domain_tags: %w[error mycomponent],
|
|
169
|
+
unresolved: true
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
46
174
|
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.
|
|
4
|
+
version: 0.1.21
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -139,44 +139,44 @@ dependencies:
|
|
|
139
139
|
name: rubocop
|
|
140
140
|
requirement: !ruby/object:Gem::Requirement
|
|
141
141
|
requirements:
|
|
142
|
-
- - "
|
|
142
|
+
- - ">="
|
|
143
143
|
- !ruby/object:Gem::Version
|
|
144
|
-
version: '
|
|
144
|
+
version: '0'
|
|
145
145
|
type: :development
|
|
146
146
|
prerelease: false
|
|
147
147
|
version_requirements: !ruby/object:Gem::Requirement
|
|
148
148
|
requirements:
|
|
149
|
-
- - "
|
|
149
|
+
- - ">="
|
|
150
150
|
- !ruby/object:Gem::Version
|
|
151
|
-
version: '
|
|
151
|
+
version: '0'
|
|
152
152
|
- !ruby/object:Gem::Dependency
|
|
153
153
|
name: rubocop-legion
|
|
154
154
|
requirement: !ruby/object:Gem::Requirement
|
|
155
155
|
requirements:
|
|
156
|
-
- - "
|
|
156
|
+
- - ">="
|
|
157
157
|
- !ruby/object:Gem::Version
|
|
158
|
-
version: '0
|
|
158
|
+
version: '0'
|
|
159
159
|
type: :development
|
|
160
160
|
prerelease: false
|
|
161
161
|
version_requirements: !ruby/object:Gem::Requirement
|
|
162
162
|
requirements:
|
|
163
|
-
- - "
|
|
163
|
+
- - ">="
|
|
164
164
|
- !ruby/object:Gem::Version
|
|
165
|
-
version: '0
|
|
165
|
+
version: '0'
|
|
166
166
|
- !ruby/object:Gem::Dependency
|
|
167
167
|
name: rubocop-rspec
|
|
168
168
|
requirement: !ruby/object:Gem::Requirement
|
|
169
169
|
requirements:
|
|
170
|
-
- - "
|
|
170
|
+
- - ">="
|
|
171
171
|
- !ruby/object:Gem::Version
|
|
172
|
-
version: '
|
|
172
|
+
version: '0'
|
|
173
173
|
type: :development
|
|
174
174
|
prerelease: false
|
|
175
175
|
version_requirements: !ruby/object:Gem::Requirement
|
|
176
176
|
requirements:
|
|
177
|
-
- - "
|
|
177
|
+
- - ">="
|
|
178
178
|
- !ruby/object:Gem::Version
|
|
179
|
-
version: '
|
|
179
|
+
version: '0'
|
|
180
180
|
description: 'LEX agentic memory domain: episodic, semantic, and working memory'
|
|
181
181
|
email:
|
|
182
182
|
- matthewdiverson@gmail.com
|