lex-tick 0.1.15 → 0.1.16
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/lib/legion/extensions/tick/helpers/constants.rb +3 -1
- data/lib/legion/extensions/tick/helpers/state.rb +19 -7
- data/lib/legion/extensions/tick/runners/orchestrator.rb +33 -4
- data/lib/legion/extensions/tick/version.rb +1 -1
- data/spec/legion/extensions/tick/helpers/constants_spec.rb +10 -2
- data/spec/legion/extensions/tick/helpers/state_spec.rb +14 -0
- data/spec/legion/extensions/tick/runners/orchestrator_spec.rb +74 -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: 94918234bd1b19854887b794867550d1a6252f74ea4ad10dd6009b6df2797c5b
|
|
4
|
+
data.tar.gz: df175d240f2a5816e9b7be4269aa12880051e1bbe5bdf6f761731cca585b0e10
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 291b85fa4cfe572c00bb279026569c08691daf1233d7900ffe619e6c4f8d15b3cc9241d18f98aa8ef22715a0438a78a1239919181decf4463f8ab26e13c708cc
|
|
7
|
+
data.tar.gz: 56b3a74946173477cbd2d3d85a6b35ff15cc61472b9065bade72af63dcbdab5a8392ceaf50a54ca7e4397db01142cf6d4ee702786d60d938bbd7fa2ff541f954
|
|
@@ -55,7 +55,9 @@ module Legion
|
|
|
55
55
|
SENTINEL_TIMEOUT = 3600 # seconds without any signal before demotion to dormant
|
|
56
56
|
DREAM_IDLE_THRESHOLD = 1800 # seconds dormant with no signal before entering dream cycle
|
|
57
57
|
SENTINEL_TO_DREAM_THRESHOLD = 600 # seconds sentinel with no signal before entering dream cycle
|
|
58
|
+
DREAM_BACKOFF_INTERVAL = 1800 # seconds after a completed dream before another dream cycle
|
|
58
59
|
MAX_TICK_DURATION = 5.0 # hard ceiling for full active tick (seconds)
|
|
60
|
+
DREAM_TICK_BUDGET = 5.0 # hard ceiling for dormant-active dream tick (seconds)
|
|
59
61
|
SENTINEL_TICK_BUDGET = 0.5 # time budget for sentinel tick
|
|
60
62
|
DORMANT_TICK_BUDGET = 0.2 # time budget for dormant tick
|
|
61
63
|
EMERGENCY_PROMOTION_BUDGET = 0.05 # max latency for emergency mode promotion
|
|
@@ -95,7 +97,7 @@ module Legion
|
|
|
95
97
|
def tick_budget(mode)
|
|
96
98
|
case mode
|
|
97
99
|
when :dormant then DORMANT_TICK_BUDGET
|
|
98
|
-
when :dormant_active then
|
|
100
|
+
when :dormant_active then DREAM_TICK_BUDGET
|
|
99
101
|
when :sentinel then SENTINEL_TICK_BUDGET
|
|
100
102
|
else MAX_TICK_DURATION
|
|
101
103
|
end
|
|
@@ -5,7 +5,7 @@ module Legion
|
|
|
5
5
|
module Tick
|
|
6
6
|
module Helpers
|
|
7
7
|
class State
|
|
8
|
-
attr_reader :mode, :tick_count, :last_signal_at, :last_high_salience_at,
|
|
8
|
+
attr_reader :mode, :tick_count, :last_signal_at, :last_high_salience_at, :last_dream_completed_at,
|
|
9
9
|
:phase_results, :current_phase, :mode_history
|
|
10
10
|
|
|
11
11
|
def initialize(mode: :dormant)
|
|
@@ -13,6 +13,7 @@ module Legion
|
|
|
13
13
|
@tick_count = 0
|
|
14
14
|
@last_signal_at = nil
|
|
15
15
|
@last_high_salience_at = nil
|
|
16
|
+
@last_dream_completed_at = nil
|
|
16
17
|
@phase_results = {}
|
|
17
18
|
@current_phase = nil
|
|
18
19
|
@mode_history = [{ mode: mode, at: Time.now.utc }]
|
|
@@ -44,6 +45,10 @@ module Legion
|
|
|
44
45
|
@mode_history.shift while @mode_history.size > 50
|
|
45
46
|
end
|
|
46
47
|
|
|
48
|
+
def record_dream_completed
|
|
49
|
+
@last_dream_completed_at = Time.now.utc
|
|
50
|
+
end
|
|
51
|
+
|
|
47
52
|
def seconds_since_signal
|
|
48
53
|
return 0.0 unless @last_signal_at
|
|
49
54
|
|
|
@@ -56,14 +61,21 @@ module Legion
|
|
|
56
61
|
Time.now.utc - @last_high_salience_at
|
|
57
62
|
end
|
|
58
63
|
|
|
64
|
+
def seconds_since_dream_completed
|
|
65
|
+
return Float::INFINITY unless @last_dream_completed_at
|
|
66
|
+
|
|
67
|
+
Time.now.utc - @last_dream_completed_at
|
|
68
|
+
end
|
|
69
|
+
|
|
59
70
|
def to_h
|
|
60
71
|
{
|
|
61
|
-
mode:
|
|
62
|
-
tick_count:
|
|
63
|
-
current_phase:
|
|
64
|
-
last_signal_at:
|
|
65
|
-
last_high_salience_at:
|
|
66
|
-
|
|
72
|
+
mode: @mode,
|
|
73
|
+
tick_count: @tick_count,
|
|
74
|
+
current_phase: @current_phase,
|
|
75
|
+
last_signal_at: @last_signal_at,
|
|
76
|
+
last_high_salience_at: @last_high_salience_at,
|
|
77
|
+
last_dream_completed_at: @last_dream_completed_at,
|
|
78
|
+
phases_completed: @phase_results.keys
|
|
67
79
|
}
|
|
68
80
|
end
|
|
69
81
|
end
|
|
@@ -25,7 +25,7 @@ module Legion
|
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def evaluate_mode_transition(signals: [], emergency: nil, dream_complete: false, **) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
28
|
+
def evaluate_mode_transition(signals: [], emergency: nil, dream_complete: false, **) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
29
29
|
state = tick_state
|
|
30
30
|
previous_mode = state.mode
|
|
31
31
|
|
|
@@ -44,7 +44,7 @@ module Legion
|
|
|
44
44
|
when :dormant
|
|
45
45
|
if signals.any?
|
|
46
46
|
:sentinel
|
|
47
|
-
elsif state.seconds_since_signal >= Helpers::Constants::DREAM_IDLE_THRESHOLD
|
|
47
|
+
elsif state.seconds_since_signal >= Helpers::Constants::DREAM_IDLE_THRESHOLD && dream_backoff_elapsed?(state)
|
|
48
48
|
:dormant_active
|
|
49
49
|
else
|
|
50
50
|
:dormant
|
|
@@ -53,6 +53,7 @@ module Legion
|
|
|
53
53
|
if max_salience >= Helpers::Constants::HIGH_SALIENCE_THRESHOLD || has_human
|
|
54
54
|
:sentinel
|
|
55
55
|
elsif dream_complete
|
|
56
|
+
state.record_dream_completed
|
|
56
57
|
:dormant
|
|
57
58
|
else
|
|
58
59
|
:dormant_active
|
|
@@ -60,10 +61,11 @@ module Legion
|
|
|
60
61
|
when :sentinel
|
|
61
62
|
if has_human || max_salience >= Helpers::Constants::HIGH_SALIENCE_THRESHOLD
|
|
62
63
|
:full_active
|
|
63
|
-
elsif state.seconds_since_signal >= Helpers::Constants::SENTINEL_TO_DREAM_THRESHOLD
|
|
64
|
-
:dormant_active
|
|
65
64
|
elsif state.seconds_since_signal >= Helpers::Constants::SENTINEL_TIMEOUT
|
|
66
65
|
:dormant
|
|
66
|
+
elsif state.seconds_since_signal >= Helpers::Constants::SENTINEL_TO_DREAM_THRESHOLD &&
|
|
67
|
+
dream_backoff_elapsed?(state)
|
|
68
|
+
:dormant_active
|
|
67
69
|
else
|
|
68
70
|
:sentinel
|
|
69
71
|
end
|
|
@@ -137,6 +139,7 @@ module Legion
|
|
|
137
139
|
context: context
|
|
138
140
|
}
|
|
139
141
|
results = run_phases(phases, state, ctx)
|
|
142
|
+
finish_dream_cycle(state, phases, results)
|
|
140
143
|
|
|
141
144
|
total_elapsed = Time.now.utc - start_time
|
|
142
145
|
skipped = phases - results.keys
|
|
@@ -187,6 +190,28 @@ module Legion
|
|
|
187
190
|
"elapsed=#{(total_elapsed * 1000).round(1)}ms#{skipped_suffix}"
|
|
188
191
|
end
|
|
189
192
|
|
|
193
|
+
def finish_dream_cycle(state, phases, results)
|
|
194
|
+
return unless state.mode == :dormant_active
|
|
195
|
+
return unless phases == Helpers::Constants::DREAM_PHASES
|
|
196
|
+
|
|
197
|
+
skipped = phases - results.keys
|
|
198
|
+
failed = failed_dream_phases(results)
|
|
199
|
+
state.record_dream_completed
|
|
200
|
+
state.transition_to(:dormant)
|
|
201
|
+
if skipped.empty? && failed.empty?
|
|
202
|
+
log.info '[tick] dream cycle complete; backing off to dormant'
|
|
203
|
+
else
|
|
204
|
+
log.info "[tick] dream cycle deferred; backing off to dormant skipped=#{skipped} failed=#{failed}"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def failed_dream_phases(results)
|
|
209
|
+
results.each_with_object([]) do |(phase, result), failed|
|
|
210
|
+
status = result.is_a?(Hash) ? result[:status] : nil
|
|
211
|
+
failed << phase if %i[error failed failure timeout].include?(status)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
190
215
|
def full_active_cooldown_elapsed(state)
|
|
191
216
|
if state.last_high_salience_at
|
|
192
217
|
state.seconds_since_high_salience
|
|
@@ -197,6 +222,10 @@ module Legion
|
|
|
197
222
|
end
|
|
198
223
|
end
|
|
199
224
|
|
|
225
|
+
def dream_backoff_elapsed?(state)
|
|
226
|
+
state.seconds_since_dream_completed >= Helpers::Constants::DREAM_BACKOFF_INTERVAL
|
|
227
|
+
end
|
|
228
|
+
|
|
200
229
|
def tick_state
|
|
201
230
|
@tick_state ||= Helpers::State.new
|
|
202
231
|
end
|
|
@@ -87,8 +87,8 @@ RSpec.describe Legion::Extensions::Tick::Helpers::Constants do
|
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
describe '.tick_budget' do
|
|
90
|
-
it 'returns
|
|
91
|
-
expect(described_class.tick_budget(:dormant_active)).to eq(
|
|
90
|
+
it 'returns DREAM_TICK_BUDGET for dormant_active' do
|
|
91
|
+
expect(described_class.tick_budget(:dormant_active)).to eq(described_class::DREAM_TICK_BUDGET)
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
94
|
|
|
@@ -100,5 +100,13 @@ RSpec.describe Legion::Extensions::Tick::Helpers::Constants do
|
|
|
100
100
|
it 'defines SENTINEL_TO_DREAM_THRESHOLD as 600' do
|
|
101
101
|
expect(described_class::SENTINEL_TO_DREAM_THRESHOLD).to eq(600)
|
|
102
102
|
end
|
|
103
|
+
|
|
104
|
+
it 'defines DREAM_BACKOFF_INTERVAL as 1800' do
|
|
105
|
+
expect(described_class::DREAM_BACKOFF_INTERVAL).to eq(1800)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'defines DREAM_TICK_BUDGET as finite' do
|
|
109
|
+
expect(described_class::DREAM_TICK_BUDGET).to eq(5.0)
|
|
110
|
+
end
|
|
103
111
|
end
|
|
104
112
|
end
|
|
@@ -60,6 +60,19 @@ RSpec.describe Legion::Extensions::Tick::Helpers::State do
|
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
describe '#record_dream_completed' do
|
|
64
|
+
it 'updates last_dream_completed_at' do
|
|
65
|
+
state.record_dream_completed
|
|
66
|
+
expect(state.last_dream_completed_at).not_to be_nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe '#seconds_since_dream_completed' do
|
|
71
|
+
it 'treats missing dream completion history as eligible for a dream cycle' do
|
|
72
|
+
expect(state.seconds_since_dream_completed).to eq(Float::INFINITY)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
63
76
|
describe '#transition_to' do
|
|
64
77
|
it 'changes mode' do
|
|
65
78
|
state.transition_to(:sentinel)
|
|
@@ -83,6 +96,7 @@ RSpec.describe Legion::Extensions::Tick::Helpers::State do
|
|
|
83
96
|
h = state.to_h
|
|
84
97
|
expect(h).to have_key(:mode)
|
|
85
98
|
expect(h).to have_key(:tick_count)
|
|
99
|
+
expect(h).to have_key(:last_dream_completed_at)
|
|
86
100
|
expect(h).to have_key(:phases_completed)
|
|
87
101
|
end
|
|
88
102
|
end
|
|
@@ -67,6 +67,56 @@ RSpec.describe Legion::Extensions::Tick::Runners::Orchestrator do
|
|
|
67
67
|
result = client.execute_tick(signals: [{ salience: 0.3 }])
|
|
68
68
|
expect(result[:mode]).to eq(:sentinel)
|
|
69
69
|
end
|
|
70
|
+
|
|
71
|
+
it 'completes one dormant_active dream cycle and backs off to dormant' do
|
|
72
|
+
client.set_mode(mode: :dormant_active)
|
|
73
|
+
|
|
74
|
+
result = client.execute_tick
|
|
75
|
+
|
|
76
|
+
expect(result[:phases_executed]).to eq(Legion::Extensions::Tick::Helpers::Constants::DREAM_PHASES)
|
|
77
|
+
expect(result[:mode]).to eq(:dormant)
|
|
78
|
+
expect(client.tick_status[:last_dream_completed_at]).not_to be_nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'does not immediately restart a completed dream cycle on the next heartbeat' do
|
|
82
|
+
state = client.send(:tick_state)
|
|
83
|
+
allow(state).to receive(:seconds_since_signal).and_return(1801.0)
|
|
84
|
+
|
|
85
|
+
client.set_mode(mode: :dormant_active)
|
|
86
|
+
client.execute_tick
|
|
87
|
+
result = client.execute_tick
|
|
88
|
+
|
|
89
|
+
expect(result[:mode]).to eq(:dormant)
|
|
90
|
+
expect(result[:phases_executed]).to eq([:memory_consolidation])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'backs off to dormant when a dream phase fails' do
|
|
94
|
+
client.set_mode(mode: :dormant_active)
|
|
95
|
+
|
|
96
|
+
result = client.execute_tick(
|
|
97
|
+
phase_handlers: {
|
|
98
|
+
dream_narration: ->(**) { { status: :error } }
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
expect(result[:mode]).to eq(:dormant)
|
|
103
|
+
expect(result[:results][:dream_narration][:status]).to eq(:error)
|
|
104
|
+
expect(client.tick_status[:last_dream_completed_at]).not_to be_nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'backs off to dormant when the dream tick budget is exhausted before all phases run' do
|
|
108
|
+
constants = Legion::Extensions::Tick::Helpers::Constants
|
|
109
|
+
allow(constants).to receive(:tick_budget).and_call_original
|
|
110
|
+
allow(constants).to receive(:tick_budget).with(:dormant_active).and_return(0.0)
|
|
111
|
+
|
|
112
|
+
client.set_mode(mode: :dormant_active)
|
|
113
|
+
result = client.execute_tick
|
|
114
|
+
|
|
115
|
+
expect(result[:mode]).to eq(:dormant)
|
|
116
|
+
expect(result[:phases_executed]).to be_empty
|
|
117
|
+
expect(result[:phases_skipped]).to eq(constants::DREAM_PHASES)
|
|
118
|
+
expect(client.tick_status[:last_dream_completed_at]).not_to be_nil
|
|
119
|
+
end
|
|
70
120
|
end
|
|
71
121
|
|
|
72
122
|
describe '#evaluate_mode_transition' do
|
|
@@ -138,6 +188,7 @@ RSpec.describe Legion::Extensions::Tick::Runners::Orchestrator do
|
|
|
138
188
|
result = client.evaluate_mode_transition(dream_complete: true)
|
|
139
189
|
expect(result[:transitioned]).to be true
|
|
140
190
|
expect(result[:new_mode]).to eq(:dormant)
|
|
191
|
+
expect(client.tick_status[:last_dream_completed_at]).not_to be_nil
|
|
141
192
|
end
|
|
142
193
|
|
|
143
194
|
it 'stays dormant_active when no signals and dream not complete' do
|
|
@@ -146,6 +197,29 @@ RSpec.describe Legion::Extensions::Tick::Runners::Orchestrator do
|
|
|
146
197
|
expect(result[:transitioned]).to be false
|
|
147
198
|
expect(result[:current_mode]).to eq(:dormant_active)
|
|
148
199
|
end
|
|
200
|
+
|
|
201
|
+
it 'keeps dormant backed off after a completed dream cycle' do
|
|
202
|
+
state = client.send(:tick_state)
|
|
203
|
+
state.record_dream_completed
|
|
204
|
+
allow(state).to receive(:seconds_since_signal).and_return(1801.0)
|
|
205
|
+
|
|
206
|
+
result = client.evaluate_mode_transition
|
|
207
|
+
|
|
208
|
+
expect(result[:transitioned]).to be false
|
|
209
|
+
expect(result[:current_mode]).to eq(:dormant)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'allows dormant_active again after dream backoff elapses' do
|
|
213
|
+
state = client.send(:tick_state)
|
|
214
|
+
state.record_dream_completed
|
|
215
|
+
allow(state).to receive(:seconds_since_signal).and_return(1801.0)
|
|
216
|
+
allow(state).to receive(:seconds_since_dream_completed).and_return(1801.0)
|
|
217
|
+
|
|
218
|
+
result = client.evaluate_mode_transition
|
|
219
|
+
|
|
220
|
+
expect(result[:transitioned]).to be true
|
|
221
|
+
expect(result[:new_mode]).to eq(:dormant_active)
|
|
222
|
+
end
|
|
149
223
|
end
|
|
150
224
|
|
|
151
225
|
context 'sentinel -> dormant_active' do
|