legionio 1.5.13 → 1.5.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 +16 -0
- data/lib/legion/cli/chat/tools/query_knowledge.rb +1 -1
- data/lib/legion/cli/chat_command.rb +11 -2
- data/lib/legion/extensions.rb +10 -0
- data/lib/legion/service.rb +87 -14
- data/lib/legion/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: d66368a89ce7ef86f5c62277fe403c840caf722f8e6b840000251775ce74e4a4
|
|
4
|
+
data.tar.gz: 8a44567875935c668bf2884e7fac6e0831f0dad9f1860f5e2bde28eedb0b7df5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7e45f6aa1d7838ed5c5ddac267310ea8163f27f1e4c8efd190720da148add55ff9f50e958c8096691d6ce3a613064322f11eba64d8fc0eae79bea26fc1ee1a9c
|
|
7
|
+
data.tar.gz: ce24287b1dd968ef9e33e053c358b7e182b92685c6696b0074fef7ab3d0447449452f9959a3086148a73b606fefc643444546ad34e596b8c8ea5039b4bf65c17
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.15] - 2026-03-25
|
|
4
|
+
|
|
5
|
+
### Removed
|
|
6
|
+
- CLI chat Apollo writeback prototype (replaced by pipeline step 19 in legion-llm)
|
|
7
|
+
|
|
8
|
+
## [1.5.14] - 2026-03-25
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Shutdown no longer hangs when network is unreachable — all component shutdowns wrapped in bounded timeouts via `shutdown_component` helper (#30)
|
|
12
|
+
- Reload path also wrapped with same timeout guards to prevent hangs during network-triggered reload (#30)
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Network watchdog: background `Concurrent::TimerTask` monitors transport/data/cache connectivity, pauses actors after sustained failures, triggers `Legion.reload` when network restores (#30)
|
|
16
|
+
- `Legion::Extensions.pause_actors` suspends all `Every` timer tasks without destroying instances (#30)
|
|
17
|
+
- Watchdog is feature-flagged via `network.watchdog.enabled` (default: false), configurable threshold and interval (#30)
|
|
18
|
+
|
|
3
19
|
## [1.5.13] - 2026-03-25
|
|
4
20
|
|
|
5
21
|
### Fixed
|
|
@@ -43,7 +43,7 @@ module Legion
|
|
|
43
43
|
private
|
|
44
44
|
|
|
45
45
|
def apollo_query(query:, domain:, limit:)
|
|
46
|
-
body = { query: query, limit: limit }
|
|
46
|
+
body = { query: query, limit: limit, status: %w[confirmed candidate] }
|
|
47
47
|
body[:domain] = domain if domain
|
|
48
48
|
|
|
49
49
|
uri = URI("http://#{DEFAULT_HOST}:#{apollo_port}/api/apollo/query")
|
|
@@ -102,10 +102,16 @@ module Legion
|
|
|
102
102
|
|
|
103
103
|
chat_log.info "headless prompt model=#{session.model_id} length=#{text.length}"
|
|
104
104
|
|
|
105
|
+
turn_tool_calls = []
|
|
106
|
+
tool_callbacks = {
|
|
107
|
+
on_tool_call: ->(tc) { turn_tool_calls << { name: tc.name, args: tc.arguments, result: nil } },
|
|
108
|
+
on_tool_result: ->(tr) { turn_tool_calls.last[:result] = tr.to_s.lines.first(3).join.rstrip if turn_tool_calls.last }
|
|
109
|
+
}
|
|
110
|
+
|
|
105
111
|
response = if options[:output_format] == 'json'
|
|
106
|
-
session.send_message(text)
|
|
112
|
+
session.send_message(text, **tool_callbacks)
|
|
107
113
|
else
|
|
108
|
-
session.send_message(text) { |chunk| print chunk.content if chunk.content }
|
|
114
|
+
session.send_message(text, **tool_callbacks) { |chunk| print chunk.content if chunk.content }
|
|
109
115
|
end
|
|
110
116
|
|
|
111
117
|
chat_log.info "headless complete tokens_in=#{session.stats[:input_tokens]} tokens_out=#{session.stats[:output_tokens]}"
|
|
@@ -298,11 +304,13 @@ module Legion
|
|
|
298
304
|
buffer = String.new
|
|
299
305
|
tool_index = 0
|
|
300
306
|
tool_total = 0
|
|
307
|
+
turn_tool_calls = []
|
|
301
308
|
@session.send_message(
|
|
302
309
|
stripped,
|
|
303
310
|
on_tool_call: lambda { |tc|
|
|
304
311
|
tool_index += 1
|
|
305
312
|
chat_log.debug "tool_call name=#{tc.name} args=#{tc.arguments.keys.join(',')}"
|
|
313
|
+
turn_tool_calls << { name: tc.name, args: tc.arguments, result: nil }
|
|
306
314
|
@session.emit(:tool_start, {
|
|
307
315
|
name: tc.name, args: tc.arguments,
|
|
308
316
|
index: tool_index, total: tool_total
|
|
@@ -312,6 +320,7 @@ module Legion
|
|
|
312
320
|
on_tool_result: lambda { |tr|
|
|
313
321
|
result_preview = tr.to_s.lines.first(3).join.rstrip
|
|
314
322
|
chat_log.debug "tool_result preview=#{result_preview[0..200]}"
|
|
323
|
+
turn_tool_calls.last[:result] = result_preview if turn_tool_calls.last
|
|
315
324
|
@session.emit(:tool_complete, {
|
|
316
325
|
name: 'tool', result_preview: result_preview,
|
|
317
326
|
index: tool_index, total: tool_total
|
data/lib/legion/extensions.rb
CHANGED
|
@@ -96,6 +96,16 @@ module Legion
|
|
|
96
96
|
Legion::Logging.info "Successfully shut down all actors (#{(Time.now - shutdown_start).round(1)}s)"
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
+
def pause_actors
|
|
100
|
+
@running_instances&.each do |inst|
|
|
101
|
+
timer = inst.instance_variable_get(:@timer)
|
|
102
|
+
timer&.shutdown if timer.respond_to?(:shutdown)
|
|
103
|
+
rescue StandardError => e
|
|
104
|
+
Legion::Logging.error "pause_actors: #{e.class}: #{e.message}" if defined?(Legion::Logging)
|
|
105
|
+
end
|
|
106
|
+
Legion::Logging.warn 'All actors paused' if defined?(Legion::Logging)
|
|
107
|
+
end
|
|
108
|
+
|
|
99
109
|
def load_extensions
|
|
100
110
|
@extensions ||= []
|
|
101
111
|
@loaded_extensions ||= []
|
data/lib/legion/service.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'timeout'
|
|
3
4
|
require_relative 'readiness'
|
|
4
5
|
require_relative 'process_role'
|
|
5
6
|
|
|
@@ -130,6 +131,7 @@ module Legion
|
|
|
130
131
|
api_settings = Legion::Settings[:api] || {}
|
|
131
132
|
@api_enabled = api && api_settings.fetch(:enabled, true)
|
|
132
133
|
setup_api if @api_enabled
|
|
134
|
+
setup_network_watchdog
|
|
133
135
|
Legion::Settings[:client][:ready] = true
|
|
134
136
|
Legion::Events.emit('service.ready')
|
|
135
137
|
end
|
|
@@ -465,13 +467,14 @@ module Legion
|
|
|
465
467
|
Legion::Settings[:client][:shutting_down] = true
|
|
466
468
|
Legion::Events.emit('service.shutting_down')
|
|
467
469
|
|
|
470
|
+
shutdown_network_watchdog
|
|
468
471
|
shutdown_audit_archiver
|
|
469
472
|
shutdown_api
|
|
470
473
|
|
|
471
474
|
Legion::Metrics.reset! if defined?(Legion::Metrics)
|
|
472
475
|
|
|
473
476
|
if defined?(Legion::Gaia) && Legion::Gaia.respond_to?(:started?) && Legion::Gaia.started?
|
|
474
|
-
Legion::Gaia.shutdown
|
|
477
|
+
shutdown_component('Gaia') { Legion::Gaia.shutdown }
|
|
475
478
|
Legion::Readiness.mark_not_ready(:gaia)
|
|
476
479
|
end
|
|
477
480
|
|
|
@@ -480,32 +483,33 @@ module Legion
|
|
|
480
483
|
@cluster_leader = nil
|
|
481
484
|
end
|
|
482
485
|
|
|
483
|
-
Legion::
|
|
486
|
+
ext_timeout = Legion::Settings.dig(:extensions, :shutdown_timeout) || 15
|
|
487
|
+
shutdown_component('Extensions', timeout: ext_timeout) { Legion::Extensions.shutdown }
|
|
484
488
|
Legion::Readiness.mark_not_ready(:extensions)
|
|
485
489
|
|
|
486
490
|
if Legion::Settings[:llm]&.dig(:connected)
|
|
487
|
-
Legion::LLM.shutdown
|
|
491
|
+
shutdown_component('LLM') { Legion::LLM.shutdown }
|
|
488
492
|
Legion::Readiness.mark_not_ready(:llm)
|
|
489
493
|
end
|
|
490
494
|
|
|
491
495
|
if defined?(Legion::Rbac) && Legion::Settings[:rbac]&.dig(:connected)
|
|
492
|
-
Legion::Rbac.shutdown
|
|
496
|
+
shutdown_component('Rbac') { Legion::Rbac.shutdown }
|
|
493
497
|
Legion::Readiness.mark_not_ready(:rbac)
|
|
494
498
|
end
|
|
495
499
|
|
|
496
|
-
Legion::Data.shutdown if Legion::Settings[:data][:connected]
|
|
500
|
+
shutdown_component('Data') { Legion::Data.shutdown } if Legion::Settings[:data][:connected]
|
|
497
501
|
Legion::Readiness.mark_not_ready(:data)
|
|
498
502
|
|
|
499
503
|
Legion::Leader.reset! if defined?(Legion::Leader)
|
|
500
504
|
|
|
501
|
-
Legion::Cache.shutdown
|
|
505
|
+
shutdown_component('Cache') { Legion::Cache.shutdown }
|
|
502
506
|
Legion::Readiness.mark_not_ready(:cache)
|
|
503
507
|
|
|
504
|
-
Legion::Transport::Connection.shutdown
|
|
508
|
+
shutdown_component('Transport') { Legion::Transport::Connection.shutdown }
|
|
505
509
|
Legion::Readiness.mark_not_ready(:transport)
|
|
506
510
|
|
|
507
511
|
shutdown_mtls_rotation
|
|
508
|
-
Legion::Crypt.shutdown
|
|
512
|
+
shutdown_component('Crypt') { Legion::Crypt.shutdown }
|
|
509
513
|
Legion::Readiness.mark_not_ready(:crypt)
|
|
510
514
|
|
|
511
515
|
Legion::Settings[:client][:ready] = false
|
|
@@ -513,29 +517,34 @@ module Legion
|
|
|
513
517
|
end
|
|
514
518
|
|
|
515
519
|
def reload
|
|
520
|
+
return if @reloading
|
|
521
|
+
|
|
522
|
+
@reloading = true
|
|
516
523
|
Legion::Logging.info 'Legion::Service.reload was called'
|
|
517
524
|
Legion::Settings[:client][:ready] = false
|
|
518
525
|
|
|
526
|
+
shutdown_network_watchdog
|
|
519
527
|
shutdown_api
|
|
520
528
|
|
|
521
529
|
if defined?(Legion::Gaia) && Legion::Gaia.respond_to?(:started?) && Legion::Gaia.started?
|
|
522
|
-
Legion::Gaia.shutdown
|
|
530
|
+
shutdown_component('Gaia') { Legion::Gaia.shutdown }
|
|
523
531
|
Legion::Readiness.mark_not_ready(:gaia)
|
|
524
532
|
end
|
|
525
533
|
|
|
526
|
-
Legion::
|
|
534
|
+
ext_timeout = Legion::Settings.dig(:extensions, :shutdown_timeout) || 15
|
|
535
|
+
shutdown_component('Extensions', timeout: ext_timeout) { Legion::Extensions.shutdown }
|
|
527
536
|
Legion::Readiness.mark_not_ready(:extensions)
|
|
528
537
|
|
|
529
|
-
Legion::Data.shutdown
|
|
538
|
+
shutdown_component('Data') { Legion::Data.shutdown }
|
|
530
539
|
Legion::Readiness.mark_not_ready(:data)
|
|
531
540
|
|
|
532
|
-
Legion::Cache.shutdown
|
|
541
|
+
shutdown_component('Cache') { Legion::Cache.shutdown }
|
|
533
542
|
Legion::Readiness.mark_not_ready(:cache)
|
|
534
543
|
|
|
535
|
-
Legion::Transport::Connection.shutdown
|
|
544
|
+
shutdown_component('Transport') { Legion::Transport::Connection.shutdown }
|
|
536
545
|
Legion::Readiness.mark_not_ready(:transport)
|
|
537
546
|
|
|
538
|
-
Legion::Crypt.shutdown
|
|
547
|
+
shutdown_component('Crypt') { Legion::Crypt.shutdown }
|
|
539
548
|
Legion::Readiness.mark_not_ready(:crypt)
|
|
540
549
|
|
|
541
550
|
Legion::Readiness.wait_until_not_ready(:transport, :data, :cache, :crypt)
|
|
@@ -569,9 +578,12 @@ module Legion
|
|
|
569
578
|
|
|
570
579
|
Legion::Crypt.cs
|
|
571
580
|
setup_api if @api_enabled
|
|
581
|
+
setup_network_watchdog
|
|
572
582
|
Legion::Settings[:client][:ready] = true
|
|
573
583
|
Legion::Events.emit('service.ready')
|
|
574
584
|
Legion::Logging.info 'Legion has been reloaded'
|
|
585
|
+
ensure
|
|
586
|
+
@reloading = false
|
|
575
587
|
end
|
|
576
588
|
|
|
577
589
|
def load_extensions
|
|
@@ -630,6 +642,67 @@ module Legion
|
|
|
630
642
|
nil
|
|
631
643
|
end
|
|
632
644
|
|
|
645
|
+
def shutdown_component(name, timeout: 5, &)
|
|
646
|
+
Timeout.timeout(timeout, &)
|
|
647
|
+
rescue Timeout::Error
|
|
648
|
+
Legion::Logging.warn "#{name} shutdown timed out after #{timeout}s, forcing"
|
|
649
|
+
rescue StandardError => e
|
|
650
|
+
Legion::Logging.warn "#{name} shutdown error: #{e.class}: #{e.message}"
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def setup_network_watchdog
|
|
654
|
+
return unless Legion::Settings.dig(:network, :watchdog, :enabled)
|
|
655
|
+
|
|
656
|
+
@consecutive_failures = Concurrent::AtomicFixnum.new(0)
|
|
657
|
+
threshold = Legion::Settings.dig(:network, :watchdog, :failure_threshold) || 5
|
|
658
|
+
interval = Legion::Settings.dig(:network, :watchdog, :check_interval) || 15
|
|
659
|
+
|
|
660
|
+
@network_watchdog = Concurrent::TimerTask.new(execution_interval: interval) do
|
|
661
|
+
if network_healthy?
|
|
662
|
+
prev = @consecutive_failures.value
|
|
663
|
+
@consecutive_failures.value = 0
|
|
664
|
+
if prev >= threshold
|
|
665
|
+
Legion::Logging.info '[Watchdog] Network restored, triggering reload'
|
|
666
|
+
Thread.new { Legion.reload } unless @reloading
|
|
667
|
+
end
|
|
668
|
+
else
|
|
669
|
+
count = @consecutive_failures.increment
|
|
670
|
+
Legion::Logging.warn "[Watchdog] Network check failed (#{count}/#{threshold})"
|
|
671
|
+
if count == threshold
|
|
672
|
+
Legion::Logging.error '[Watchdog] Network failure threshold reached, pausing actors'
|
|
673
|
+
Legion::Extensions.pause_actors if Legion::Extensions.respond_to?(:pause_actors)
|
|
674
|
+
end
|
|
675
|
+
end
|
|
676
|
+
rescue StandardError => e
|
|
677
|
+
Legion::Logging.debug "[Watchdog] check error: #{e.message}"
|
|
678
|
+
end
|
|
679
|
+
@network_watchdog.execute
|
|
680
|
+
Legion::Logging.info "[Watchdog] Network watchdog started (interval=#{interval}s, threshold=#{threshold})"
|
|
681
|
+
rescue StandardError => e
|
|
682
|
+
Legion::Logging.warn "Network watchdog setup failed: #{e.message}"
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
def shutdown_network_watchdog
|
|
686
|
+
@network_watchdog&.shutdown
|
|
687
|
+
@network_watchdog = nil
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def network_healthy?
|
|
691
|
+
return true if defined?(Legion::Transport::Connection) && Legion::Transport::Connection.lite_mode?
|
|
692
|
+
|
|
693
|
+
checks = []
|
|
694
|
+
checks << Legion::Transport::Connection.session_open? if Legion::Settings[:transport][:connected]
|
|
695
|
+
if Legion::Settings[:data][:connected] && defined?(Legion::Data::Connection)
|
|
696
|
+
checks << (Legion::Data::Connection.sequel&.test_connection rescue false) # rubocop:disable Style/RescueModifier
|
|
697
|
+
end
|
|
698
|
+
checks << Legion::Cache.connected? if Legion::Settings[:cache][:connected] && defined?(Legion::Cache)
|
|
699
|
+
return true if checks.empty?
|
|
700
|
+
|
|
701
|
+
checks.any?
|
|
702
|
+
rescue StandardError
|
|
703
|
+
false
|
|
704
|
+
end
|
|
705
|
+
|
|
633
706
|
private
|
|
634
707
|
|
|
635
708
|
def port_in_use?(bind, port)
|
data/lib/legion/version.rb
CHANGED