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 +4 -4
- data/CHANGELOG.md +13 -0
- data/lib/legion/extensions/synapse/actors/blast_radius.rb +1 -1
- data/lib/legion/extensions/synapse/actors/decay.rb +1 -1
- data/lib/legion/extensions/synapse/actors/homeostasis.rb +28 -14
- data/lib/legion/extensions/synapse/runners/blast_radius.rb +1 -1
- data/lib/legion/extensions/synapse/runners/evaluate.rb +30 -17
- data/lib/legion/extensions/synapse/runners/mutate.rb +19 -16
- data/lib/legion/extensions/synapse/runners/pain.rb +9 -0
- data/lib/legion/extensions/synapse/runners/revert.rb +39 -38
- data/lib/legion/extensions/synapse/version.rb +1 -1
- 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: '037082609e6545e0a3581981a602d49afdd4a532cf4b15d166f0cf04fd3a746f'
|
|
4
|
+
data.tar.gz: 42c81c769ef9f4367dec25d4713d5ab4bc4eb1491b88478bb365d5cbd3e2521e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
@@ -11,42 +11,56 @@ module Legion
|
|
|
11
11
|
def check_subtask? = false
|
|
12
12
|
def generate_task? = false
|
|
13
13
|
|
|
14
|
-
def action(**
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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[:
|
|
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
|
-
|
|
74
|
-
|
|
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:
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
attention:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|