lex-agentic-memory 0.1.13 → 0.1.16
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 +17 -0
- data/lib/legion/extensions/agentic/memory/trace/helpers/postgres_store.rb +20 -14
- data/lib/legion/extensions/agentic/memory/trace/helpers/snapshot.rb +77 -12
- data/lib/legion/extensions/agentic/memory/version.rb +1 -1
- data/lib/legion/extensions/agentic/memory.rb +9 -26
- data/spec/legion/extensions/agentic/memory/trace/helpers/postgres_store_spec.rb +47 -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: 53ea02bfdd236c2e64023525d767a238c9dfa28d96db8993cee882a2105bcccb
|
|
4
|
+
data.tar.gz: 442b45ad5cdacc49197156b364db727cdabbb0ebf2f30881f823b7b04f776871
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6c3ce2299ecd503a25069b865c7e68db33de87b90d4419879a45810119b92310f1816d140baf082014cf3bbcc920b9bc52342154db1c9db132b8ad6ba91fa0b
|
|
7
|
+
data.tar.gz: b2712151eae4cf64f51bfa92792210e4200b84c0c0caa71830d4361470fe6f38d04c2775d6cd1faba777180499fdef18120946d1e8a3b37e60262db7be241d2b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.16] - 2026-03-29
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Add `=> e` capture and `Legion::Logging.error` call to all bare `rescue StandardError` clauses in `memory.rb` to satisfy rescue-logging lint rule
|
|
7
|
+
- Fix `Lint/ShadowedException` in `snapshot.rb`: replace `rescue NameError, NoMethodError` with `rescue NameError` (`NoMethodError` is a subclass of `NameError`)
|
|
8
|
+
- Refactor `Snapshot#distribute_state` into dedicated helper methods to reduce cyclomatic/perceived complexity below threshold
|
|
9
|
+
|
|
10
|
+
## [0.1.15] - 2026-03-26
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- fix remote_invocable? to use class method for local dispatch
|
|
14
|
+
|
|
15
|
+
## [0.1.14] - 2026-03-26
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- `PostgresStore#serialize_trace` and `#map_update_fields` now strip null bytes (`\x00`) from all string fields before INSERT/UPDATE. PostgreSQL text columns reject null bytes, causing `string contains null byte` errors when content from external sources (e.g., Teams Graph API) contains embedded nulls
|
|
19
|
+
|
|
3
20
|
## [0.1.13] - 2026-03-26
|
|
4
21
|
|
|
5
22
|
### Fixed
|
|
@@ -286,28 +286,28 @@ module Legion
|
|
|
286
286
|
trace_id: trace[:trace_id],
|
|
287
287
|
agent_id: @agent_id,
|
|
288
288
|
tenant_id: @tenant_id,
|
|
289
|
-
trace_type: trace[:trace_type].to_s,
|
|
290
|
-
content: payload.is_a?(Hash) ? Legion::JSON.dump(payload) : payload.to_s,
|
|
289
|
+
trace_type: sanitize_pg_string(trace[:trace_type].to_s),
|
|
290
|
+
content: sanitize_pg_string(payload.is_a?(Hash) ? Legion::JSON.dump(payload) : payload.to_s),
|
|
291
291
|
significance: conf,
|
|
292
292
|
confidence: conf,
|
|
293
|
-
associations: assocs.is_a?(Array) ? Legion::JSON.dump(assocs) : '[]',
|
|
294
|
-
domain_tags: tags.is_a?(Array)
|
|
293
|
+
associations: sanitize_pg_string(assocs.is_a?(Array) ? Legion::JSON.dump(assocs) : '[]'),
|
|
294
|
+
domain_tags: sanitize_pg_string(tags.is_a?(Array) ? Legion::JSON.dump(tags) : nil),
|
|
295
295
|
strength: trace[:strength],
|
|
296
296
|
peak_strength: trace[:peak_strength],
|
|
297
297
|
base_decay_rate: trace[:base_decay_rate],
|
|
298
298
|
emotional_valence: ev.is_a?(Numeric) ? ev.to_f : 0.0,
|
|
299
299
|
emotional_intensity: trace[:emotional_intensity],
|
|
300
|
-
origin: trace[:origin].to_s,
|
|
301
|
-
source_agent_id: trace[:source_agent_id],
|
|
302
|
-
storage_tier: trace[:storage_tier].to_s,
|
|
300
|
+
origin: sanitize_pg_string(trace[:origin].to_s),
|
|
301
|
+
source_agent_id: sanitize_pg_string(trace[:source_agent_id]),
|
|
302
|
+
storage_tier: sanitize_pg_string(trace[:storage_tier].to_s),
|
|
303
303
|
last_reinforced: trace[:last_reinforced],
|
|
304
304
|
last_decayed: trace[:last_decayed],
|
|
305
305
|
reinforcement_count: trace[:reinforcement_count],
|
|
306
306
|
unresolved: trace[:unresolved] || false,
|
|
307
307
|
consolidation_candidate: trace[:consolidation_candidate] || false,
|
|
308
|
-
parent_trace_id: trace[:parent_trace_id],
|
|
309
|
-
encryption_key_id: trace[:encryption_key_id],
|
|
310
|
-
partition_id: trace[:partition_id],
|
|
308
|
+
parent_trace_id: sanitize_pg_string(trace[:parent_trace_id]),
|
|
309
|
+
encryption_key_id: sanitize_pg_string(trace[:encryption_key_id]),
|
|
310
|
+
partition_id: sanitize_pg_string(trace[:partition_id]),
|
|
311
311
|
created_at: trace[:created_at] || Time.now.utc,
|
|
312
312
|
accessed_at: Time.now.utc
|
|
313
313
|
}
|
|
@@ -359,13 +359,13 @@ module Legion
|
|
|
359
359
|
|
|
360
360
|
row[col] = case col
|
|
361
361
|
when :content
|
|
362
|
-
v.is_a?(Hash) ? Legion::JSON.dump(v) : v.to_s
|
|
362
|
+
sanitize_pg_string(v.is_a?(Hash) ? Legion::JSON.dump(v) : v.to_s)
|
|
363
363
|
when :associations
|
|
364
|
-
v.is_a?(Array) ? Legion::JSON.dump(v) : '[]'
|
|
364
|
+
sanitize_pg_string(v.is_a?(Array) ? Legion::JSON.dump(v) : '[]')
|
|
365
365
|
when :domain_tags
|
|
366
|
-
v.is_a?(Array) ? Legion::JSON.dump(v) : nil
|
|
366
|
+
sanitize_pg_string(v.is_a?(Array) ? Legion::JSON.dump(v) : nil)
|
|
367
367
|
when :trace_type, :origin, :storage_tier
|
|
368
|
-
v.to_s
|
|
368
|
+
sanitize_pg_string(v.to_s)
|
|
369
369
|
else
|
|
370
370
|
v
|
|
371
371
|
end
|
|
@@ -390,6 +390,12 @@ module Legion
|
|
|
390
390
|
[]
|
|
391
391
|
end
|
|
392
392
|
|
|
393
|
+
def sanitize_pg_string(value)
|
|
394
|
+
return value unless value.is_a?(String)
|
|
395
|
+
|
|
396
|
+
value.delete("\x00")
|
|
397
|
+
end
|
|
398
|
+
|
|
393
399
|
def log_warn(message)
|
|
394
400
|
Legion::Logging.warn "[memory:postgres_store] #{message}" if defined?(Legion::Logging)
|
|
395
401
|
end
|
|
@@ -12,6 +12,8 @@ module Legion
|
|
|
12
12
|
module Helpers
|
|
13
13
|
module Snapshot
|
|
14
14
|
class << self
|
|
15
|
+
include Legion::Logging::Helper if defined?(Legion::Logging::Helper)
|
|
16
|
+
|
|
15
17
|
def save_snapshot(agent_id:)
|
|
16
18
|
state = gather_state(agent_id)
|
|
17
19
|
packed = MessagePack.pack(state)
|
|
@@ -37,16 +39,13 @@ module Legion
|
|
|
37
39
|
signature = raw[-64..]
|
|
38
40
|
packed = raw[0..-65]
|
|
39
41
|
|
|
40
|
-
unless verify_data(packed, signature, agent_id)
|
|
41
|
-
Legion::Logging.warn "[snapshot] signature verification failed for #{agent_id}" if defined?(Legion::Logging)
|
|
42
|
-
return { success: false, reason: :invalid_signature }
|
|
43
|
-
end
|
|
42
|
+
return { success: false, reason: :invalid_signature } unless verify_data(packed, signature, agent_id)
|
|
44
43
|
|
|
45
44
|
state = MessagePack.unpack(packed, symbolize_keys: true)
|
|
46
45
|
distribute_state(state)
|
|
47
46
|
{ success: true, agent_id: agent_id, timestamp: state[:timestamp] }
|
|
48
47
|
rescue StandardError => e
|
|
49
|
-
|
|
48
|
+
log.error(e.message) if respond_to?(:log)
|
|
50
49
|
{ success: false, reason: :error, message: e.message }
|
|
51
50
|
end
|
|
52
51
|
|
|
@@ -97,24 +96,90 @@ module Legion
|
|
|
97
96
|
[]
|
|
98
97
|
end
|
|
99
98
|
|
|
100
|
-
state[:personality_state] =
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
state[:personality_state] =
|
|
100
|
+
if defined?(Legion::Extensions::Agentic::Self) &&
|
|
101
|
+
Legion::Extensions::Agentic::Self.respond_to?(:personality_snapshot)
|
|
102
|
+
Legion::Extensions::Agentic::Self.personality_snapshot
|
|
103
|
+
else
|
|
104
|
+
{}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
state[:mood_state] =
|
|
108
|
+
if defined?(Legion::Extensions::Agentic::Affect) &&
|
|
109
|
+
Legion::Extensions::Agentic::Affect.respond_to?(:mood_snapshot)
|
|
110
|
+
Legion::Extensions::Agentic::Affect.mood_snapshot
|
|
111
|
+
else
|
|
112
|
+
{}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
state[:trust_scores] =
|
|
116
|
+
if defined?(Legion::Mesh) && Legion::Mesh.respond_to?(:trust_snapshot)
|
|
117
|
+
Legion::Mesh.trust_snapshot
|
|
118
|
+
else
|
|
119
|
+
{}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
state[:reflection_history] =
|
|
123
|
+
if defined?(Legion::Extensions::Agentic::Self) &&
|
|
124
|
+
Legion::Extensions::Agentic::Self.respond_to?(:reflection_snapshot)
|
|
125
|
+
Legion::Extensions::Agentic::Self.reflection_snapshot
|
|
126
|
+
else
|
|
127
|
+
[]
|
|
128
|
+
end
|
|
129
|
+
|
|
104
130
|
state
|
|
105
131
|
end
|
|
106
132
|
|
|
107
133
|
def distribute_state(state)
|
|
108
|
-
|
|
134
|
+
restore_memory_traces(state[:memory_traces])
|
|
135
|
+
restore_personality(state[:personality_state])
|
|
136
|
+
restore_mood(state[:mood_state])
|
|
137
|
+
restore_trust_scores(state[:trust_scores])
|
|
138
|
+
restore_reflections(state[:reflection_history])
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def restore_memory_traces(traces)
|
|
142
|
+
return unless traces
|
|
109
143
|
|
|
110
144
|
store = Legion::Extensions::Agentic::Memory::Trace.shared_store
|
|
111
145
|
if store.respond_to?(:restore_traces)
|
|
112
|
-
store.restore_traces(
|
|
146
|
+
store.restore_traces(traces)
|
|
113
147
|
elsif store.respond_to?(:store)
|
|
114
|
-
|
|
148
|
+
traces.each { |t| store.store(t) }
|
|
115
149
|
end
|
|
116
150
|
end
|
|
117
151
|
|
|
152
|
+
def restore_personality(personality_state)
|
|
153
|
+
return unless personality_state && !personality_state.empty?
|
|
154
|
+
return unless defined?(Legion::Extensions::Agentic::Self) &&
|
|
155
|
+
Legion::Extensions::Agentic::Self.respond_to?(:restore_personality)
|
|
156
|
+
|
|
157
|
+
Legion::Extensions::Agentic::Self.restore_personality(personality_state)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def restore_mood(mood_state)
|
|
161
|
+
return unless mood_state && !mood_state.empty?
|
|
162
|
+
return unless defined?(Legion::Extensions::Agentic::Affect) &&
|
|
163
|
+
Legion::Extensions::Agentic::Affect.respond_to?(:restore_mood)
|
|
164
|
+
|
|
165
|
+
Legion::Extensions::Agentic::Affect.restore_mood(mood_state)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def restore_trust_scores(trust_scores)
|
|
169
|
+
return unless trust_scores && !trust_scores.empty?
|
|
170
|
+
return unless defined?(Legion::Mesh) && Legion::Mesh.respond_to?(:restore_trust)
|
|
171
|
+
|
|
172
|
+
Legion::Mesh.restore_trust(trust_scores)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def restore_reflections(reflection_history)
|
|
176
|
+
return unless reflection_history && !reflection_history.empty?
|
|
177
|
+
return unless defined?(Legion::Extensions::Agentic::Self) &&
|
|
178
|
+
Legion::Extensions::Agentic::Self.respond_to?(:restore_reflections)
|
|
179
|
+
|
|
180
|
+
Legion::Extensions::Agentic::Self.restore_reflections(reflection_history)
|
|
181
|
+
end
|
|
182
|
+
|
|
118
183
|
def sign_data(data, _agent_id)
|
|
119
184
|
if defined?(Legion::Crypt) && Legion::Crypt.respond_to?(:ed25519_sign)
|
|
120
185
|
signature = Legion::Crypt.ed25519_sign(data)
|
|
@@ -26,7 +26,7 @@ module Legion
|
|
|
26
26
|
module Memory
|
|
27
27
|
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
28
28
|
|
|
29
|
-
def remote_invocable?
|
|
29
|
+
def self.remote_invocable?
|
|
30
30
|
false
|
|
31
31
|
end
|
|
32
32
|
end
|
|
@@ -36,41 +36,24 @@ end
|
|
|
36
36
|
|
|
37
37
|
# Snapshot lifecycle hooks
|
|
38
38
|
if defined?(Legion::Events)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
rescue StandardError
|
|
42
|
-
true
|
|
43
|
-
end
|
|
39
|
+
settings_loaded = defined?(Legion::Settings)
|
|
40
|
+
snapshot_enabled = settings_loaded ? Legion::Settings.dig(:snapshot, :enabled) : true
|
|
44
41
|
if snapshot_enabled
|
|
45
42
|
require 'legion/extensions/agentic/memory/trace/helpers/snapshot'
|
|
46
43
|
|
|
47
44
|
Legion::Events.on('service.shutting_down') do
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
rescue StandardError
|
|
51
|
-
true
|
|
52
|
-
end
|
|
45
|
+
auto_save = settings_loaded ? Legion::Settings.dig(:snapshot, :auto_save_on_shutdown) : true
|
|
46
|
+
next unless auto_save
|
|
53
47
|
|
|
54
|
-
agent_id =
|
|
55
|
-
Legion::Settings.dig(:agent, :id)
|
|
56
|
-
rescue StandardError
|
|
57
|
-
nil
|
|
58
|
-
end || 'default'
|
|
48
|
+
agent_id = (settings_loaded ? Legion::Settings.dig(:agent, :id) : nil) || 'default'
|
|
59
49
|
Legion::Extensions::Agentic::Memory::Trace::Helpers::Snapshot.save_snapshot(agent_id: agent_id)
|
|
60
50
|
end
|
|
61
51
|
|
|
62
52
|
Legion::Events.once('gaia.started') do
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
rescue StandardError
|
|
66
|
-
true
|
|
67
|
-
end
|
|
53
|
+
auto_restore = settings_loaded ? Legion::Settings.dig(:snapshot, :auto_restore_on_boot) : true
|
|
54
|
+
next unless auto_restore
|
|
68
55
|
|
|
69
|
-
agent_id =
|
|
70
|
-
Legion::Settings.dig(:agent, :id)
|
|
71
|
-
rescue StandardError
|
|
72
|
-
nil
|
|
73
|
-
end || 'default'
|
|
56
|
+
agent_id = (settings_loaded ? Legion::Settings.dig(:agent, :id) : nil) || 'default'
|
|
74
57
|
Legion::Extensions::Agentic::Memory::Trace::Helpers::Snapshot.restore_snapshot(agent_id: agent_id)
|
|
75
58
|
end
|
|
76
59
|
end
|
|
@@ -185,6 +185,53 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::PostgresStor
|
|
|
185
185
|
end
|
|
186
186
|
end
|
|
187
187
|
|
|
188
|
+
# --- null byte sanitization ---
|
|
189
|
+
|
|
190
|
+
describe 'null byte sanitization' do
|
|
191
|
+
it 'strips null bytes from string content and stores successfully' do
|
|
192
|
+
trace = trace_helper.new_trace(type: :episodic, content_payload: "hello\x00world")
|
|
193
|
+
result = store.store(trace)
|
|
194
|
+
expect(result).not_to be_nil
|
|
195
|
+
|
|
196
|
+
retrieved = store.retrieve(trace[:trace_id])
|
|
197
|
+
expect(retrieved[:content_payload]).to eq('helloworld')
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it 'strips null bytes from hash content payloads' do
|
|
201
|
+
trace = trace_helper.new_trace(type: :episodic, content_payload: { text: "has\x00null" })
|
|
202
|
+
result = store.store(trace)
|
|
203
|
+
expect(result).not_to be_nil
|
|
204
|
+
|
|
205
|
+
row = db[:memory_traces].where(trace_id: trace[:trace_id]).first
|
|
206
|
+
expect(row[:content]).not_to include("\x00")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it 'strips null bytes from domain_tags' do
|
|
210
|
+
trace = trace_helper.new_trace(type: :episodic, content_payload: 'clean', domain_tags: ["tag\x00bad"])
|
|
211
|
+
store.store(trace)
|
|
212
|
+
|
|
213
|
+
row = db[:memory_traces].where(trace_id: trace[:trace_id]).first
|
|
214
|
+
expect(row[:domain_tags]).not_to include("\x00")
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'stores cleanly when no null bytes are present' do
|
|
218
|
+
trace = trace_helper.new_trace(type: :episodic, content_payload: 'no nulls here')
|
|
219
|
+
result = store.store(trace)
|
|
220
|
+
expect(result).not_to be_nil
|
|
221
|
+
|
|
222
|
+
retrieved = store.retrieve(trace[:trace_id])
|
|
223
|
+
expect(retrieved[:content_payload]).to eq('no nulls here')
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it 'strips null bytes during partial update' do
|
|
227
|
+
store.store(semantic_trace)
|
|
228
|
+
store.update(semantic_trace[:trace_id], content_payload: { text: "up\x00dated" })
|
|
229
|
+
|
|
230
|
+
row = db[:memory_traces].where(trace_id: semantic_trace[:trace_id]).first
|
|
231
|
+
expect(row[:content]).not_to include("\x00")
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
188
235
|
# --- retrieve_by_type ---
|
|
189
236
|
|
|
190
237
|
describe '#retrieve_by_type' do
|