lex-synapse 0.4.13 → 0.4.15

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: 140dd32d856f6a661d423c72a6f7fcc2424a789342e72d656c25302903d7eda9
4
- data.tar.gz: 89651542649d74fc871576b608172a4d941cb6b25322a073a54a947458eb369e
3
+ metadata.gz: '037082609e6545e0a3581981a602d49afdd4a532cf4b15d166f0cf04fd3a746f'
4
+ data.tar.gz: 42c81c769ef9f4367dec25d4713d5ab4bc4eb1491b88478bb365d5cbd3e2521e
5
5
  SHA512:
6
- metadata.gz: c852c9b3dad49309789b63563244983b075a5d13711efd46d3c83118bd0ec49058d7b7a1a9e4279ea0a9445ec3c3184845701b3389e70676960f3fad7fb74f5a
7
- data.tar.gz: bd6602b930def1ccac2ef61933c659844f959bd219d06267c696143f04802058eda62ccebaa988a45707424b73fd4c2ccdace4207809dabe173c5e61150e11ad
6
+ metadata.gz: a5f60904a36dcf8fb276c75683485a7c4ef8207b9b1f1b6c03cae33bd392fa85d4fc44beb605aeae62f492be50be546b90b209021361d1105c9adc43306bfe21
7
+ data.tar.gz: '059f003e2a808fe705e315b8c7c75c01631ac97ff86c2f675467dd563f37106ea039fc4aaddf32ad89315d4290226e0308d209c0e3df8ebd89644584ab315de9'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.15] - 2026-10-29
4
+ ### Fixed
5
+ - Treat missing configured conditioner/transformer capabilities as unavailable rather than successful learning; retain legitimate observe/filter pass-through.
6
+ - Attribute reactive proposals to the signal created by that evaluation and report actual transformation execution.
7
+ - Restore standalone pain-triggered reversion, ignore pending evaluations in completed-outcome failure streaks, and return restored confidence.
8
+ - Make mutation/revert writes transactional and keep history versions unambiguous across repeated reversion, retaining actual before/after snapshots.
9
+
10
+ ## [0.4.14] - 2026-06-01
11
+ ### Fixed
12
+ - Homeostasis actor: convert raw signal counts to signals/minute for correct spike/drought comparison; batch all updates in a single DB transaction
13
+ - Evaluate runner: prevent double confidence adjustment when validation fails (`run_transform` already adjusts)
14
+ - Revert mutation: use unique version (`synapse.version + 1`) instead of `restored_version` to avoid version collision
15
+
3
16
  ## [0.4.13] - 2026-05-07
4
17
  ### Fixed
5
18
  - `Homeostasis` actor: replace per-synapse `signals_dataset.count` with a single batched `GROUP BY` query to eliminate N+1 pool contention that caused `Sequel::PoolTimeout` on Postgres
@@ -31,7 +31,7 @@ module Legion
31
31
  false
32
32
  end
33
33
 
34
- def action(**_opts)
34
+ def action(**)
35
35
  compute
36
36
  end
37
37
  end
@@ -11,7 +11,7 @@ module Legion
11
11
  def check_subtask? = false
12
12
  def generate_task? = false
13
13
 
14
- def action(**_opts)
14
+ def action(**)
15
15
  return { status: :skipped, reason: :no_data } unless defined?(Legion::Data)
16
16
 
17
17
  Data::Model.define_synapse_model
@@ -11,42 +11,56 @@ module Legion
11
11
  def check_subtask? = false
12
12
  def generate_task? = false
13
13
 
14
- def action(**_opts)
14
+ def action(**)
15
15
  return { status: :skipped, reason: :no_data } unless defined?(Legion::Data)
16
16
 
17
17
  results = { spikes: 0, droughts: 0, updated: 0 }
18
18
  return results unless defined?(Legion::Extensions::Synapse::Data::Model::Synapse)
19
19
 
20
20
  cutoff = Time.now - 60
21
+ window_seconds = 60.0
22
+ signal_model = Legion::Extensions::Synapse::Data::Model::SynapseSignal
23
+ synapse_model = Legion::Extensions::Synapse::Data::Model::Synapse
21
24
 
22
- active_synapses = Legion::Extensions::Synapse::Data::Model::Synapse
23
- .where(status: 'active')
24
- .where { baseline_throughput > 0 } # rubocop:disable Style/NumericPredicate
25
- .all
25
+ # Single query: count signals per synapse in the last 60s window
26
+ signal_counts = signal_model.where { created_at > cutoff }
27
+ .group_and_count(:synapse_id)
28
+ .as_hash(:synapse_id, :count)
26
29
 
30
+ # Fetch only active synapses with a nonzero baseline — eager-load to avoid N+1
31
+ active_synapses = synapse_model.where(status: 'active')
32
+ .where { baseline_throughput > 0 } # rubocop:disable Style/NumericPredicate
33
+ .all
27
34
  return results if active_synapses.empty?
28
35
 
29
- signal_counts = Legion::Extensions::Synapse::Data::Model::SynapseSignal
30
- .where(synapse_id: active_synapses.map(&:id))
31
- .where { created_at > cutoff }
32
- .group_and_count(:synapse_id)
33
- .as_hash(:synapse_id, :count)
36
+ # Collect updates in memory, then apply in a single batch to avoid connection churn
37
+ updates = []
34
38
 
35
39
  active_synapses.each do |synapse|
36
40
  baseline = synapse.baseline_throughput
37
- current = signal_counts.fetch(synapse.id, 0).to_f
41
+ # Convert raw count in the window to signals/minute for apples-to-apples comparison
42
+ current = (signal_counts.fetch(synapse.id, 0).to_f / window_seconds) * 60.0
38
43
 
39
- if Helpers::Homeostasis.spike?(current, baseline, duration_seconds: 60)
44
+ if Helpers::Homeostasis.spike?(current, baseline, duration_seconds: window_seconds)
40
45
  results[:spikes] += 1
41
- elsif Helpers::Homeostasis.drought?(current, baseline, silent_seconds: 60)
46
+ elsif Helpers::Homeostasis.drought?(current, baseline, silent_seconds: window_seconds)
42
47
  results[:droughts] += 1
43
48
  end
44
49
 
45
50
  new_baseline = Helpers::Homeostasis.update_baseline(baseline, current)
46
- synapse.update(baseline_throughput: new_baseline)
51
+ updates << { synapse_id: synapse.id, baseline_throughput: new_baseline }
47
52
  results[:updated] += 1
48
53
  end
49
54
 
55
+ # Batch-update all baselines in a single transaction
56
+ unless updates.empty?
57
+ synapse_model.db.transaction do
58
+ updates.each do |u|
59
+ synapse_model.where(id: u[:synapse_id]).update(baseline_throughput: u[:baseline_throughput])
60
+ end
61
+ end
62
+ end
63
+
50
64
  results
51
65
  end
52
66
  end
@@ -20,7 +20,7 @@ module Legion
20
20
  TIER_CRITICAL => 3.0
21
21
  }.freeze
22
22
 
23
- def compute(**_opts)
23
+ def compute(**)
24
24
  Data::Model.define_synapse_model
25
25
  return { success: false, error: 'data unavailable' } unless data_available?
26
26
 
@@ -27,6 +27,14 @@ module Legion
27
27
 
28
28
  # Step 1: Attention check
29
29
  attention_result = check_attention(synapse, payload, mode, conditioner_client)
30
+ # Unavailable execution is not evidence for confidence or proposal learning.
31
+ if attention_result[:unavailable]
32
+ return {
33
+ success: mode == :observe, mode: mode, passed: attention_result[:passed],
34
+ transformed: false, result: payload, error: attention_result[:error],
35
+ latency_ms: ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond) - start_time
36
+ }
37
+ end
30
38
 
31
39
  # Step 2: Transform (if attention passed and confidence allows)
32
40
  transform_result = if attention_result[:passed] && Helpers::Confidence.can_transform?(synapse.confidence)
@@ -36,18 +44,26 @@ module Legion
36
44
  end
37
45
 
38
46
  elapsed = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond) - start_time
47
+ if transform_result[:unavailable]
48
+ return {
49
+ success: false, mode: mode, passed: attention_result[:passed],
50
+ transformed: false, result: payload, error: transform_result[:error], latency_ms: elapsed
51
+ }
52
+ end
39
53
 
40
54
  # Step 3: Record signal
41
- record_signal(synapse, attention_result[:passed], transform_result[:success], elapsed)
42
-
43
- # Step 4: Adjust confidence
44
- event = transform_result[:success] ? :success : :failure
45
- new_confidence = Helpers::Confidence.adjust(synapse.confidence, event)
46
- synapse.update(confidence: new_confidence)
55
+ signal_record = record_signal(synapse, attention_result[:passed], transform_result[:success], elapsed)
56
+
57
+ # Step 4: Adjust confidence (skip if run_transform already penalized for validation failure)
58
+ new_confidence = synapse.confidence
59
+ unless transform_result[:validation_failure]
60
+ event = transform_result[:success] ? :success : :failure
61
+ new_confidence = Helpers::Confidence.adjust(synapse.confidence, event)
62
+ synapse.update(confidence: new_confidence)
63
+ end
47
64
 
48
65
  # Step 5: Generate proposals if autonomous
49
66
  if Helpers::Confidence.can_self_modify?(new_confidence) && Helpers::Proposals.reactive?
50
- signal_record = Data::Model::SynapseSignal.where(synapse_id: synapse.id).order(Sequel.desc(:id)).first
51
67
  propose_reactive(
52
68
  synapse: synapse, payload: payload, signal_id: signal_record&.id,
53
69
  attention_result: attention_result, transform_result: transform_result,
@@ -59,7 +75,7 @@ module Legion
59
75
  success: transform_result[:success],
60
76
  mode: mode,
61
77
  passed: attention_result[:passed],
62
- transformed: transform_result[:success],
78
+ transformed: transform_result[:transformed] == true,
63
79
  result: transform_result[:result],
64
80
  latency_ms: elapsed
65
81
  }
@@ -69,13 +85,10 @@ module Legion
69
85
 
70
86
  def check_attention(synapse, payload, mode, conditioner_client)
71
87
  return { passed: true } if synapse.attention.nil? || synapse.attention.empty?
88
+ return { passed: mode == :observe, unavailable: true, error: 'conditioner unavailable' } unless conditioner_client.respond_to?(:evaluate)
72
89
 
73
- passed = if conditioner_client
74
- result = conditioner_client.evaluate(conditions: Legion::JSON.load(synapse.attention), values: payload)
75
- result[:valid]
76
- else
77
- true
78
- end
90
+ result = conditioner_client.evaluate(conditions: Legion::JSON.load(synapse.attention), values: payload)
91
+ passed = result[:valid]
79
92
 
80
93
  # In OBSERVE mode, always pass through regardless of result
81
94
  passed = true if mode == :observe
@@ -85,7 +98,7 @@ module Legion
85
98
 
86
99
  def run_transform(synapse, payload, transformer_client)
87
100
  return { success: true, result: payload } if synapse.transform.nil? || synapse.transform.empty?
88
- return { success: true, result: payload } unless transformer_client
101
+ return { success: false, result: payload, unavailable: true, error: 'transformer unavailable' } unless transformer_client.respond_to?(:transform)
89
102
 
90
103
  transform_def = Legion::JSON.load(synapse.transform)
91
104
  result = transformer_client.transform(
@@ -96,11 +109,11 @@ module Legion
96
109
  )
97
110
 
98
111
  if result[:success]
99
- { success: true, result: result[:result] }
112
+ { success: true, transformed: true, result: result[:result] }
100
113
  else
101
114
  new_conf = Helpers::Confidence.adjust(synapse.confidence, :validation_failure)
102
115
  synapse.update(confidence: new_conf)
103
- { success: false, result: payload, error: result[:errors] }
116
+ { success: false, result: payload, error: result[:errors], validation_failure: true }
104
117
  end
105
118
  end
106
119
 
@@ -19,23 +19,25 @@ module Legion
19
19
  return { success: false, error: "invalid mutation_type: #{mutation_type}" } unless VALID_MUTATION_TYPES.include?(mutation_type)
20
20
  return { success: false, error: "invalid trigger: #{trigger}" } unless VALID_TRIGGERS.include?(trigger)
21
21
 
22
- before_state = snapshot(synapse)
23
- apply_changes(synapse, mutation_type, changes)
24
- after_state = snapshot(synapse)
22
+ synapse.db.transaction do
23
+ synapse.lock!
24
+ before_state = snapshot(synapse)
25
+ new_version = [synapse.version, synapse.mutations_dataset.max(:version) || 1].max + 1
26
+ apply_changes(synapse, mutation_type, changes)
27
+ synapse.update(version: new_version)
28
+ after_state = snapshot(synapse)
25
29
 
26
- new_version = synapse.version + 1
27
- synapse.update(version: new_version)
30
+ Data::Model::SynapseMutation.create(
31
+ synapse_id: synapse.id,
32
+ version: new_version,
33
+ mutation_type: mutation_type,
34
+ before_state: Legion::JSON.dump(before_state),
35
+ after_state: Legion::JSON.dump(after_state),
36
+ trigger: trigger
37
+ )
28
38
 
29
- Data::Model::SynapseMutation.create(
30
- synapse_id: synapse.id,
31
- version: new_version,
32
- mutation_type: mutation_type,
33
- before_state: Legion::JSON.dump(before_state),
34
- after_state: Legion::JSON.dump(after_state),
35
- trigger: trigger
36
- )
37
-
38
- { success: true, version: new_version, synapse_id: synapse.id }
39
+ { success: true, version: new_version, synapse_id: synapse.id }
40
+ end
39
41
  end
40
42
 
41
43
  private
@@ -46,7 +48,8 @@ module Legion
46
48
  transform: synapse.transform,
47
49
  routing_strategy: synapse.routing_strategy,
48
50
  confidence: synapse.confidence,
49
- status: synapse.status
51
+ status: synapse.status,
52
+ version: synapse.version
50
53
  }
51
54
  end
52
55
 
@@ -3,12 +3,15 @@
3
3
  require_relative '../helpers/confidence'
4
4
  require_relative '../data/models/synapse'
5
5
  require_relative '../data/models/synapse_signal'
6
+ require_relative 'revert'
6
7
 
7
8
  module Legion
8
9
  module Extensions
9
10
  module Synapse
10
11
  module Runners
11
12
  module Pain
13
+ include Legion::Extensions::Synapse::Runners::Revert
14
+
12
15
  CONSECUTIVE_FAILURE_THRESHOLD = 3
13
16
 
14
17
  def handle_pain(synapse_id:, task_id: nil)
@@ -40,6 +43,11 @@ module Legion
40
43
  result[:action] = :auto_revert
41
44
  revert_result = revert(synapse_id: synapse_id, trigger: 'pain')
42
45
  result[:reverted] = revert_result[:success] == true
46
+ if result[:reverted]
47
+ synapse.refresh
48
+ new_confidence = synapse.confidence
49
+ result[:confidence] = new_confidence
50
+ end
43
51
  end
44
52
 
45
53
  # Check if confidence dropped below autonomy threshold
@@ -59,6 +67,7 @@ module Legion
59
67
 
60
68
  def count_consecutive_failures(synapse)
61
69
  recent = synapse.signals_dataset
70
+ .exclude(downstream_outcome: nil)
62
71
  .order(Sequel.desc(:id))
63
72
  .limit(CONSECUTIVE_FAILURE_THRESHOLD)
64
73
  .all
@@ -14,44 +14,45 @@ module Legion
14
14
  synapse = Data::Model::Synapse[synapse_id]
15
15
  return { success: false, error: 'synapse not found' } unless synapse
16
16
 
17
- # to_version specifies which mutation record to look up (by version number).
18
- # Reverting mutation at version N restores its before_state, landing at N-1.
19
- mutation_version = to_version || synapse.version
20
- return { success: false, error: 'no previous version' } if mutation_version < 2
21
-
22
- mutation = Data::Model::SynapseMutation.where(
23
- synapse_id: synapse.id,
24
- version: mutation_version
25
- ).first
26
-
27
- return { success: false, error: "mutation version #{mutation_version} not found" } unless mutation
28
-
29
- restored_version = mutation_version - 1
30
- before_state = Legion::JSON.load(mutation.before_state)
31
- synapse.update(
32
- attention: before_state[:attention],
33
- transform: before_state[:transform],
34
- routing_strategy: before_state[:routing_strategy],
35
- confidence: before_state[:confidence] || synapse.confidence,
36
- status: before_state[:status] || synapse.status,
37
- version: restored_version
38
- )
39
-
40
- # Mark the reverted mutation
41
- mutation.update(outcome: 'reverted')
42
-
43
- # Record the revert as a new mutation
44
- Data::Model::SynapseMutation.create(
45
- synapse_id: synapse.id,
46
- version: restored_version,
47
- mutation_type: 'confidence_changed',
48
- before_state: mutation.after_state,
49
- after_state: mutation.before_state,
50
- trigger: trigger,
51
- outcome: 'reverted'
52
- )
53
-
54
- { success: true, reverted_to: restored_version, synapse_id: synapse.id }
17
+ synapse.db.transaction do
18
+ synapse.lock!
19
+ mutation_version = to_version || synapse.version
20
+ next { success: false, error: 'no previous version' } if mutation_version < 2
21
+
22
+ mutation = Data::Model::SynapseMutation.where(
23
+ synapse_id: synapse.id,
24
+ version: mutation_version
25
+ ).first
26
+
27
+ next { success: false, error: "mutation version #{mutation_version} not found" } unless mutation
28
+
29
+ before_state = Legion::JSON.load(mutation.before_state)
30
+ restored_version = before_state[:version] || (mutation_version - 1)
31
+ revert_version = [synapse.version, synapse.mutations_dataset.max(:version) || 1].max + 1
32
+ current_state = synapse.values.slice(:attention, :transform, :routing_strategy, :confidence, :status, :version)
33
+ synapse.update(
34
+ attention: before_state[:attention],
35
+ transform: before_state[:transform],
36
+ routing_strategy: before_state[:routing_strategy],
37
+ confidence: before_state[:confidence] || synapse.confidence,
38
+ status: before_state[:status] || synapse.status,
39
+ version: restored_version
40
+ )
41
+
42
+ mutation.update(outcome: 'reverted')
43
+ restored_state = synapse.values.slice(:attention, :transform, :routing_strategy, :confidence, :status, :version)
44
+ Data::Model::SynapseMutation.create(
45
+ synapse_id: synapse.id,
46
+ version: revert_version,
47
+ mutation_type: 'confidence_changed',
48
+ before_state: Legion::JSON.dump(current_state),
49
+ after_state: Legion::JSON.dump(restored_state),
50
+ trigger: trigger,
51
+ outcome: 'reverted'
52
+ )
53
+
54
+ { success: true, reverted_to: restored_version, synapse_id: synapse.id }
55
+ end
55
56
  end
56
57
  end
57
58
  end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Synapse
6
- VERSION = '0.4.13'
6
+ VERSION = '0.4.15'
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-synapse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.13
4
+ version: 0.4.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity