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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0814efaa2ca66f0121dd6d783ec6383b09542f126628695cf2c375c056b5c2b1'
4
- data.tar.gz: d6df0516525b8ff2883f9f4074eb488ac6df00c74ee24994ab7c629a9a449a0e
3
+ metadata.gz: 53ea02bfdd236c2e64023525d767a238c9dfa28d96db8993cee882a2105bcccb
4
+ data.tar.gz: 442b45ad5cdacc49197156b364db727cdabbb0ebf2f30881f823b7b04f776871
5
5
  SHA512:
6
- metadata.gz: ad170b27af1c27a90430d7ce82d89554b5a4cd5709504535d84bfc3ae52046495a790acf29cc9665739c2486cc736c36cbd118cf588f4f9418d3e90f307182e3
7
- data.tar.gz: 8ee023a4360585eca2bd215dfc8601e4c9d2d96935a985c1bc8b0200e7953dbcd2f49d26032c35d23f8b5af69fa097a0d2c9c189291faa189e5892e22785405c
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) ? Legion::JSON.dump(tags) : nil,
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
- Legion::Logging.warn "[snapshot] restore failed: #{e.message}" if defined?(Legion::Logging)
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
- state[:mood_state] = {}
102
- state[:trust_scores] = {}
103
- state[:reflection_history] = []
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
- return unless state[:memory_traces]
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(state[:memory_traces])
146
+ store.restore_traces(traces)
113
147
  elsif store.respond_to?(:store)
114
- state[:memory_traces].each { |t| store.store(t) }
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)
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Agentic
6
6
  module Memory
7
- VERSION = '0.1.13'
7
+ VERSION = '0.1.16'
8
8
  end
9
9
  end
10
10
  end
@@ -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
- snapshot_enabled = begin
40
- Legion::Settings.dig(:snapshot, :enabled)
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
- next unless begin
49
- Legion::Settings.dig(:snapshot, :auto_save_on_shutdown)
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 = begin
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
- next unless begin
64
- Legion::Settings.dig(:snapshot, :auto_restore_on_boot)
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 = begin
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
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.13
4
+ version: 0.1.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity