lex-agentic-memory 0.1.39 → 0.1.41

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: 89cf392450f3ff2b43d9ff1430c9cb4e8b108d0221b46aa4c2e3e064514c6200
4
- data.tar.gz: 0343f1b89ae601f477939c5890fca5bcb2e8e4961f411049612731edf16ae43e
3
+ metadata.gz: 7031061b84f2f2923ab2c4d43d472fe67bb8814f32a9b85d4dbd4444d734370a
4
+ data.tar.gz: dff1be7d4e8abf1757fb98934942b3ec7996dac01ce82bcf608d6b01a5a0adfd
5
5
  SHA512:
6
- metadata.gz: ca0b83053d099fd5e445427105023210371b4e4048961bfa4d3446b8ca1412902c8c6dbbcfaa0881d787d55ba5ec70fc5321023374c3a9c35b6718d99e9af8b8
7
- data.tar.gz: 5c6e4acf09c5cafae4694253381e963316fe898e9c738bb9ae0143e9788e242e01b5dd442f3187ebb8f997b1a3ef095fdb1c1fd9a77c0ac2072c0e255247b860
6
+ metadata.gz: b7c7d56ceca01dd24ec41483d78c325ae46641efbba2a1e2d25a34ca79361c5ccd983691436d705e2ae5ce6dceacdd56e5d02ee82579424126b3594cfbfdd467
7
+ data.tar.gz: 11dc7a02ab36704d3d67ffde18045dda343e7bbb7308e357cfb0d6b60092432be51568f156ae614c5daa2be0da479d889689db76bbd5b486d786b1d9ee11d34b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.41] - 2026-07-22
4
+ ### Added
5
+ - `Runners::Traces#erase_partner!(identity:)` — deletes all traces tagged `partner:<identity>` and `owner:<identity>`; returns `{ erased: true, identity:, count: }`. Prep for H8 death protocol cascade (Phase 4.5 contract gate gap).
6
+
7
+ ## [0.1.40] - 2026-06-17
8
+ ### Fixed
9
+ - **Critical performance fix:** Trace store `snapshot_dirty_state` no longer serializes ALL traces to JSON on every flush — replaced boolean `@traces_dirty` flag with per-trace `@dirty_trace_ids` Set so only modified traces are serialized (O(changed) instead of O(total))
10
+ - `load_from_local` no longer re-serializes every trace on boot — stores raw DB rows directly in `@persisted_trace_rows`
11
+ - `persist_dirty_traces` now handles explicit deletion tracking via `@deleted_trace_ids` Set
12
+ - Eliminates 97%+ single-core CPU usage caused by `decay_cycle` triggering full-store JSON serialization every 60 seconds
13
+
3
14
  ## [0.1.39] - 2026-06-01
4
15
  ### Fixed
5
16
  - `ErrorTracer` now guards against infinite recursion — if downstream trace storage triggers error/fatal logging, the `tracing?` thread-local flag prevents re-entry
@@ -22,7 +22,8 @@ module Legion
22
22
  @traces = {}
23
23
  @associations = Hash.new { |h, k| h[k] = Hash.new(0) }
24
24
  @partition_id = partition_id || resolve_partition_id
25
- @traces_dirty = false
25
+ @dirty_trace_ids = Set.new
26
+ @deleted_trace_ids = Set.new
26
27
  @associations_dirty = false
27
28
  @persisted_trace_rows = {}
28
29
  load_from_local
@@ -32,7 +33,7 @@ module Legion
32
33
  persisted_trace = Helpers::Trace.normalize_trace_affect(trace)
33
34
  persisted_trace[:partition_id] ||= @partition_id
34
35
  @mutex.synchronize do
35
- @traces_dirty = true if @traces[persisted_trace[:trace_id]] != persisted_trace
36
+ @dirty_trace_ids << persisted_trace[:trace_id]
36
37
  @traces[persisted_trace[:trace_id]] = persisted_trace
37
38
  end
38
39
  persisted_trace[:trace_id]
@@ -50,7 +51,10 @@ module Legion
50
51
  removed_links = @associations.delete(trace_id)
51
52
  @associations.each_value { |links| links.delete(trace_id) }
52
53
 
53
- @traces_dirty = true if removed_trace
54
+ if removed_trace
55
+ @dirty_trace_ids.delete(trace_id)
56
+ @deleted_trace_ids << trace_id
57
+ end
54
58
  @associations_dirty = true if removed_trace || removed_links
55
59
  end
56
60
  end
@@ -92,8 +96,10 @@ module Legion
92
96
  @associations_dirty = true
93
97
 
94
98
  threshold = Helpers::Trace::COACTIVATION_THRESHOLD
95
- @traces_dirty = true if @associations[trace_id_a][trace_id_b] >= threshold &&
96
- link_traces(trace_id_a, trace_id_b)
99
+ if @associations[trace_id_a][trace_id_b] >= threshold && link_traces(trace_id_a, trace_id_b)
100
+ @dirty_trace_ids << trace_id_a
101
+ @dirty_trace_ids << trace_id_b
102
+ end
97
103
  end
98
104
  end
99
105
 
@@ -128,7 +134,8 @@ module Legion
128
134
  @mutex.synchronize do
129
135
  @traces = snapshot
130
136
  @associations = Hash.new { |h, k| h[k] = Hash.new(0) }
131
- @traces_dirty = true
137
+ @dirty_trace_ids = Set.new(@traces.keys)
138
+ @deleted_trace_ids = Set.new
132
139
  @associations_dirty = true
133
140
  end
134
141
  flush
@@ -183,27 +190,41 @@ module Legion
183
190
  end
184
191
 
185
192
  def snapshot_dirty_state
186
- traces_snapshot, associations_snapshot, trace_rows_snapshot, trace_changes, associations_dirty = @mutex.synchronize do
187
- ts = @traces.transform_values(&:dup)
193
+ @mutex.synchronize do
194
+ dirty_ids = @dirty_trace_ids.to_a
195
+ deleted_ids = @deleted_trace_ids.to_a
196
+ associations_dirty = @associations_dirty
197
+
198
+ return nil if dirty_ids.empty? && deleted_ids.empty? && !associations_dirty
199
+
200
+ dirty_rows = dirty_ids.each_with_object({}) do |trace_id, h|
201
+ trace = @traces[trace_id]
202
+ h[trace_id] = serialize_trace_for_db(trace) if trace
203
+ end
204
+
205
+ trace_rows_snapshot = @persisted_trace_rows.merge(dirty_rows)
206
+ deleted_ids.each { |id| trace_rows_snapshot.delete(id) }
207
+
208
+ traces_snapshot = @traces.transform_values(&:dup)
188
209
  as = @associations.each_with_object({}) { |(tid, targets), memo| memo[tid] = targets.dup }
189
- trs = ts.transform_values { |trace| serialize_trace_for_db(trace) }
190
- changed_trace_ids = trs.each_key.reject { |trace_id| trs[trace_id] == @persisted_trace_rows[trace_id] }
191
- trace_changes = { dirty: @traces_dirty || changed_trace_ids.any?, changed_ids: changed_trace_ids }
192
- [ts, as, trs, trace_changes, @associations_dirty]
193
- end
194
- return nil unless trace_changes[:dirty] || associations_dirty
210
+ trace_changes = { dirty: true, changed_ids: dirty_ids, deleted_ids: deleted_ids }
195
211
 
196
- [traces_snapshot, associations_snapshot, trace_rows_snapshot, trace_changes, associations_dirty]
212
+ [traces_snapshot, as, trace_rows_snapshot, trace_changes, associations_dirty]
213
+ end
197
214
  end
198
215
 
199
216
  def persist_dirty_traces(db, trace_rows_snapshot, trace_changes, stale_ids)
200
- return unless trace_changes[:dirty] || !stale_ids.empty?
217
+ changed_ids = trace_changes[:changed_ids] || []
218
+ deleted_ids = trace_changes[:deleted_ids] || []
219
+ all_removals = (stale_ids + deleted_ids).uniq
220
+ return if changed_ids.empty? && all_removals.empty?
201
221
 
202
222
  ds = db[:memory_traces]
203
- trace_changes[:changed_ids].each do |trace_id|
204
- ds.insert_conflict(:replace).insert(trace_rows_snapshot.fetch(trace_id))
223
+ changed_ids.each do |trace_id|
224
+ row = trace_rows_snapshot[trace_id]
225
+ ds.insert_conflict(:replace).insert(row) if row
205
226
  end
206
- db[:memory_traces].where(trace_id: stale_ids).delete unless stale_ids.empty?
227
+ db[:memory_traces].where(trace_id: all_removals).delete unless all_removals.empty?
207
228
  end
208
229
 
209
230
  def persist_dirty_associations(db, associations_snapshot, scoped_trace_ids, memory_trace_ids, stale_ids, dirty)
@@ -232,7 +253,8 @@ module Legion
232
253
 
233
254
  def clear_dirty_flags(trace_rows_snapshot)
234
255
  @mutex.synchronize do
235
- @traces_dirty = false
256
+ @dirty_trace_ids.clear
257
+ @deleted_trace_ids.clear
236
258
  @associations_dirty = false
237
259
  @persisted_trace_rows = trace_rows_snapshot
238
260
  end
@@ -246,12 +268,13 @@ module Legion
246
268
 
247
269
  db[:memory_traces].where(partition_id: @partition_id).each do |row|
248
270
  @traces[row[:trace_id]] = deserialize_trace_from_db(row)
271
+ @persisted_trace_rows[row[:trace_id]] = row.dup
249
272
  end
250
273
 
251
274
  load_local_associations(db)
252
275
 
253
- @persisted_trace_rows = @traces.transform_values { |trace| serialize_trace_for_db(trace) }
254
- @traces_dirty = false
276
+ @dirty_trace_ids = Set.new
277
+ @deleted_trace_ids = Set.new
255
278
  @associations_dirty = false
256
279
  end
257
280
 
@@ -74,6 +74,20 @@ module Legion
74
74
  { deleted: true, trace_id: trace_id }
75
75
  end
76
76
 
77
+ def erase_partner!(identity:, store: nil, **)
78
+ store ||= default_store
79
+ partner_result = store.retrieve_by_domain("partner:#{identity}", min_strength: 0.0, limit: 10_000)
80
+ owner_result = store.retrieve_by_domain("owner:#{identity}", min_strength: 0.0, limit: 10_000)
81
+ traces_to_erase = Array(partner_result) + Array(owner_result)
82
+
83
+ traces_to_erase.each { |t| store.delete(t[:trace_id]) }
84
+ persist_store(store) unless traces_to_erase.empty?
85
+
86
+ count = traces_to_erase.size
87
+ log.info("[memory] erase_partner! identity=#{identity} erased=#{count}")
88
+ { erased: true, identity: identity, count: count }
89
+ end
90
+
77
91
  def retrieve_and_reinforce(limit: 10, store: nil, **)
78
92
  store ||= default_store
79
93
  all = store.all_traces(min_strength: 0.1)
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Agentic
6
6
  module Memory
7
- VERSION = '0.1.39'
7
+ VERSION = '0.1.41'
8
8
  end
9
9
  end
10
10
  end
@@ -66,4 +66,50 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Runners::Traces do
66
66
  expect(result[:found]).to be false
67
67
  end
68
68
  end
69
+
70
+ describe '#erase_partner!' do
71
+ it 'erases all partner-tagged traces for the given identity' do
72
+ 3.times { client.store_trace(type: :semantic, content_payload: {}, domain_tags: ['partner:alice']) }
73
+ 2.times { client.store_trace(type: :semantic, content_payload: {}, domain_tags: ['partner:bob']) }
74
+
75
+ result = client.erase_partner!(identity: 'alice')
76
+
77
+ expect(result[:erased]).to be true
78
+ expect(result[:identity]).to eq('alice')
79
+ expect(result[:count]).to eq(3)
80
+
81
+ expect(client.retrieve_by_domain(domain_tag: 'partner:alice')[:count]).to eq(0)
82
+ expect(client.retrieve_by_domain(domain_tag: 'partner:bob')[:count]).to eq(2)
83
+ end
84
+
85
+ it 'erases owner-tagged traces for the given identity' do
86
+ 2.times { client.store_trace(type: :semantic, content_payload: {}, domain_tags: ['owner:alice']) }
87
+ client.store_trace(type: :semantic, content_payload: {}, domain_tags: ['partner:alice'])
88
+
89
+ result = client.erase_partner!(identity: 'alice')
90
+
91
+ expect(result[:count]).to eq(3)
92
+ expect(client.retrieve_by_domain(domain_tag: 'owner:alice')[:count]).to eq(0)
93
+ expect(client.retrieve_by_domain(domain_tag: 'partner:alice')[:count]).to eq(0)
94
+ end
95
+
96
+ it 'returns count: 0 when no traces exist for the identity' do
97
+ result = client.erase_partner!(identity: 'ghost')
98
+
99
+ expect(result[:erased]).to be true
100
+ expect(result[:identity]).to eq('ghost')
101
+ expect(result[:count]).to eq(0)
102
+ end
103
+
104
+ it 'does not erase traces belonging to other identities' do
105
+ client.store_trace(type: :semantic, content_payload: {}, domain_tags: ['partner:alice'])
106
+ client.store_trace(type: :semantic, content_payload: {}, domain_tags: ['partner:bob'])
107
+ client.store_trace(type: :semantic, content_payload: {}, domain_tags: ['owner:carol'])
108
+
109
+ client.erase_partner!(identity: 'alice')
110
+
111
+ expect(client.retrieve_by_domain(domain_tag: 'partner:bob')[:count]).to eq(1)
112
+ expect(client.retrieve_by_domain(domain_tag: 'owner:carol')[:count]).to eq(1)
113
+ end
114
+ end
69
115
  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.39
4
+ version: 0.1.41
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity