lex-agentic-memory 0.1.12 → 0.1.14

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: 6f9d21093f342adfd656a2775f74aa1e53a397c16c484b4826bfa076d06799d7
4
- data.tar.gz: 917b0430e8fe409bbe1e857d9e3989fe722405fc7a0c746de463663552aea51c
3
+ metadata.gz: 50c80158ba856d35e47e3c72fdfbadb29a4a75b0e45b1e859f507dafc850ab4d
4
+ data.tar.gz: e8ca09283855617c11b3cef8da5bdeec38a1aaeea526f1558610d1702ee8f97e
5
5
  SHA512:
6
- metadata.gz: baa0bf1ac1b2e8b49af95680e021643df0c85fd1e9eed423fc41327d7aae4e52c93b951eed9662754469e9d2ed3df63be17d22793b91d7678e2cbf61a69aa841
7
- data.tar.gz: 6463bbc07b183c771179ce3447b0846fb5b6b48065c16931c439ef5124e2e81091e13079a5c50ab6189acf5646c7fba010ae869949ef053de3612485aa21ec4c
6
+ metadata.gz: 9c7d4a3b2ba13f3639fcb6a6b8729ddef7e0e5286eaf2fb7a68fe5d4d395f6306e8dccf5d1a3c7ed2f884a49e2ddcb812f2377f0bca36384ad6231804d90a40b
7
+ data.tar.gz: d55c7a895b14acb28209eafba19b83059a1a89e84335d0e130968d941814c3af4ee66bb6d4c95a0d5c655bbe117b753f3240eee80f00cbbdccaa3e7479d322fd
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.14] - 2026-03-26
4
+
5
+ ### Fixed
6
+ - `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
7
+
8
+ ## [0.1.13] - 2026-03-26
9
+
10
+ ### Fixed
11
+ - `PostgresStore#serialize_trace` omitted `agent_id` column, causing `PG::NotNullViolation` on every insert against PostgreSQL (migration 022 declares `agent_id null: false`). Constructor now accepts `agent_id:` with fallback to `Legion::Settings.dig(:agent, :id)` or `'default'`
12
+ - `Trace.create_store` factory now resolves and passes `agent_id` to PostgresStore
13
+ - Spec schema for PostgresStore now includes `agent_id` column matching production migration
14
+
3
15
  ## [0.1.12] - 2026-03-26
4
16
 
5
17
  ### Fixed
@@ -15,8 +15,9 @@ module Legion
15
15
  TRACES_TABLE = :memory_traces
16
16
  ASSOCIATIONS_TABLE = :memory_associations
17
17
 
18
- def initialize(tenant_id: nil)
18
+ def initialize(tenant_id: nil, agent_id: nil)
19
19
  @tenant_id = tenant_id
20
+ @agent_id = agent_id || resolve_agent_id
20
21
  end
21
22
 
22
23
  # Store (upsert) a trace by trace_id.
@@ -262,6 +263,12 @@ module Legion
262
263
  Legion::Data.connection
263
264
  end
264
265
 
266
+ def resolve_agent_id
267
+ Legion::Settings.dig(:agent, :id) || 'default'
268
+ rescue StandardError
269
+ 'default'
270
+ end
271
+
265
272
  # Dataset for memory_traces scoped by tenant_id (if set).
266
273
  def traces_ds
267
274
  ds = db[TRACES_TABLE]
@@ -277,29 +284,30 @@ module Legion
277
284
 
278
285
  {
279
286
  trace_id: trace[:trace_id],
287
+ agent_id: @agent_id,
280
288
  tenant_id: @tenant_id,
281
- trace_type: trace[:trace_type].to_s,
282
- 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),
283
291
  significance: conf,
284
292
  confidence: conf,
285
- associations: assocs.is_a?(Array) ? Legion::JSON.dump(assocs) : '[]',
286
- 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),
287
295
  strength: trace[:strength],
288
296
  peak_strength: trace[:peak_strength],
289
297
  base_decay_rate: trace[:base_decay_rate],
290
298
  emotional_valence: ev.is_a?(Numeric) ? ev.to_f : 0.0,
291
299
  emotional_intensity: trace[:emotional_intensity],
292
- origin: trace[:origin].to_s,
293
- source_agent_id: trace[:source_agent_id],
294
- 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),
295
303
  last_reinforced: trace[:last_reinforced],
296
304
  last_decayed: trace[:last_decayed],
297
305
  reinforcement_count: trace[:reinforcement_count],
298
306
  unresolved: trace[:unresolved] || false,
299
307
  consolidation_candidate: trace[:consolidation_candidate] || false,
300
- parent_trace_id: trace[:parent_trace_id],
301
- encryption_key_id: trace[:encryption_key_id],
302
- 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]),
303
311
  created_at: trace[:created_at] || Time.now.utc,
304
312
  accessed_at: Time.now.utc
305
313
  }
@@ -351,13 +359,13 @@ module Legion
351
359
 
352
360
  row[col] = case col
353
361
  when :content
354
- 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)
355
363
  when :associations
356
- v.is_a?(Array) ? Legion::JSON.dump(v) : '[]'
364
+ sanitize_pg_string(v.is_a?(Array) ? Legion::JSON.dump(v) : '[]')
357
365
  when :domain_tags
358
- v.is_a?(Array) ? Legion::JSON.dump(v) : nil
366
+ sanitize_pg_string(v.is_a?(Array) ? Legion::JSON.dump(v) : nil)
359
367
  when :trace_type, :origin, :storage_tier
360
- v.to_s
368
+ sanitize_pg_string(v.to_s)
361
369
  else
362
370
  v
363
371
  end
@@ -382,6 +390,12 @@ module Legion
382
390
  []
383
391
  end
384
392
 
393
+ def sanitize_pg_string(value)
394
+ return value unless value.is_a?(String)
395
+
396
+ value.delete("\x00")
397
+ end
398
+
385
399
  def log_warn(message)
386
400
  Legion::Logging.warn "[memory:postgres_store] #{message}" if defined?(Legion::Logging)
387
401
  end
@@ -34,7 +34,7 @@ module Legion
34
34
  def create_store
35
35
  if postgres_available?
36
36
  Legion::Logging.debug '[memory] Using shared PostgresStore (write-through)'
37
- Helpers::PostgresStore.new(tenant_id: resolve_tenant_id)
37
+ Helpers::PostgresStore.new(tenant_id: resolve_tenant_id, agent_id: resolve_agent_id)
38
38
  elsif defined?(Legion::Cache) && Legion::Cache.respond_to?(:connected?) && Legion::Cache.connected?
39
39
  Legion::Logging.debug '[memory] Using shared CacheStore (memcached)'
40
40
  Helpers::CacheStore.new
@@ -60,6 +60,12 @@ module Legion
60
60
  rescue StandardError
61
61
  nil
62
62
  end
63
+
64
+ def resolve_agent_id
65
+ Legion::Settings.dig(:agent, :id) || 'default'
66
+ rescue StandardError
67
+ 'default'
68
+ end
63
69
  end
64
70
  end
65
71
  end
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Agentic
6
6
  module Memory
7
- VERSION = '0.1.12'
7
+ VERSION = '0.1.14'
8
8
  end
9
9
  end
10
10
  end
@@ -12,6 +12,7 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::PostgresStor
12
12
  d.create_table(:memory_traces) do
13
13
  primary_key :id
14
14
  String :trace_id, size: 36, null: false, unique: true
15
+ String :agent_id, size: 64, null: false, default: 'test-agent'
15
16
  String :tenant_id, size: 64
16
17
  String :trace_type, null: false
17
18
  String :content, text: true, null: false
@@ -127,6 +128,23 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::PostgresStor
127
128
  end
128
129
  end
129
130
 
131
+ # --- store agent_id population ---
132
+
133
+ describe '#store agent_id population' do
134
+ it 'writes agent_id to the database row' do
135
+ store.store(semantic_trace)
136
+ row = db[:memory_traces].where(trace_id: semantic_trace[:trace_id]).first
137
+ expect(row[:agent_id]).not_to be_nil
138
+ end
139
+
140
+ it 'uses the resolved agent_id from settings' do
141
+ custom_store = described_class.new(tenant_id: tenant_id, agent_id: 'my-agent')
142
+ custom_store.store(semantic_trace)
143
+ row = db[:memory_traces].where(trace_id: semantic_trace[:trace_id]).first
144
+ expect(row[:agent_id]).to eq('my-agent')
145
+ end
146
+ end
147
+
130
148
  # --- store + retrieve ---
131
149
 
132
150
  describe '#store and #retrieve' do
@@ -167,6 +185,53 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::PostgresStor
167
185
  end
168
186
  end
169
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
+
170
235
  # --- retrieve_by_type ---
171
236
 
172
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.12
4
+ version: 0.1.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity