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 +4 -4
- data/CHANGELOG.md +12 -0
- data/lib/legion/extensions/agentic/memory/trace/helpers/postgres_store.rb +29 -15
- data/lib/legion/extensions/agentic/memory/trace.rb +7 -1
- data/lib/legion/extensions/agentic/memory/version.rb +1 -1
- data/spec/legion/extensions/agentic/memory/trace/helpers/postgres_store_spec.rb +65 -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: 50c80158ba856d35e47e3c72fdfbadb29a4a75b0e45b1e859f507dafc850ab4d
|
|
4
|
+
data.tar.gz: e8ca09283855617c11b3cef8da5bdeec38a1aaeea526f1558610d1702ee8f97e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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)
|
|
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
|
|
@@ -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
|