legionio 1.5.8 → 1.5.9
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/.rubocop.yml +1 -0
- data/CHANGELOG.md +10 -0
- data/legionio.gemspec +4 -4
- data/lib/legion/cli/features_command.rb +272 -0
- data/lib/legion/cli.rb +4 -0
- data/lib/legion/extensions/actors/subscription.rb +4 -0
- data/lib/legion/extensions/helpers/base.rb +7 -2
- data/lib/legion/extensions/transport.rb +2 -2
- data/lib/legion/extensions.rb +66 -9
- data/lib/legion/service.rb +2 -1
- data/lib/legion/version.rb +1 -1
- metadata +10 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e7ed91b745c34247336b6d3c1d040b6773d21d531445ea7b42149cdbf4bfc170
|
|
4
|
+
data.tar.gz: 1eedc2e5b6011c0ed4a9b35d65e9be14bb474b46851720a30667e1167060633a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d89facba54ed5b92539bb40615837412b6e091283ce13fcfa6a4ac460fd204a80e103327c873afedaf62f81aa45a6a4390f97c6a945841b8fef9cc25c6a7a699
|
|
7
|
+
data.tar.gz: c298fc10efbc5a2d0a3ce69ccdc2c36976fc2f86700f3d7589ac060e97a3517cc47ea22807c9755da25390d8b2d22080abe523feba9e4b3aa6406a0ddd83ce0d
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.9] - 2026-03-25
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- `Subscription#activate` nil guard — skip activate when `@consumer` is nil (prepare failed silently)
|
|
7
|
+
- `Extensions#shutdown` tracks real actor instances in `@running_instances`, cancels them with deadline-based drain
|
|
8
|
+
- `Extensions::Helpers::Base` runner_class derivation improvements for self-contained actors
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Bumped gemspec dependencies: legion-cache >= 1.3.16, legion-settings >= 1.3.19, legion-transport >= 1.4.0, legion-mcp >= 0.5.1
|
|
12
|
+
|
|
3
13
|
## [1.5.8] - 2026-03-24
|
|
4
14
|
|
|
5
15
|
### Added
|
data/legionio.gemspec
CHANGED
|
@@ -52,13 +52,13 @@ Gem::Specification.new do |spec|
|
|
|
52
52
|
spec.add_dependency 'thor', '>= 1.3'
|
|
53
53
|
spec.add_dependency 'tty-spinner', '~> 0.9'
|
|
54
54
|
|
|
55
|
-
spec.add_dependency 'legion-cache', '>= 1.3.
|
|
56
|
-
spec.add_dependency 'legion-crypt', '>= 1.4.
|
|
55
|
+
spec.add_dependency 'legion-cache', '>= 1.3.16'
|
|
56
|
+
spec.add_dependency 'legion-crypt', '>= 1.4.12'
|
|
57
57
|
spec.add_dependency 'legion-data', '>= 1.5.0'
|
|
58
58
|
spec.add_dependency 'legion-json', '>= 1.2.1'
|
|
59
59
|
spec.add_dependency 'legion-logging', '>= 1.3.2'
|
|
60
|
-
spec.add_dependency 'legion-settings', '>= 1.3.
|
|
61
|
-
spec.add_dependency 'legion-transport', '>= 1.
|
|
60
|
+
spec.add_dependency 'legion-settings', '>= 1.3.19'
|
|
61
|
+
spec.add_dependency 'legion-transport', '>= 1.4.0'
|
|
62
62
|
|
|
63
63
|
spec.add_dependency 'legion-tty', '>= 0.4.34'
|
|
64
64
|
spec.add_dependency 'lex-node'
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'English'
|
|
4
|
+
require 'thor'
|
|
5
|
+
require 'rbconfig'
|
|
6
|
+
require 'legion/cli/output'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module CLI
|
|
10
|
+
class Features < Thor
|
|
11
|
+
namespace 'features'
|
|
12
|
+
|
|
13
|
+
def self.exit_on_failure?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class_option :json, type: :boolean, default: false, desc: 'Output as JSON'
|
|
18
|
+
class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
|
|
19
|
+
|
|
20
|
+
BUNDLES = {
|
|
21
|
+
tasking: {
|
|
22
|
+
label: 'Tasking Engine',
|
|
23
|
+
description: 'Task scheduling, chaining, conditioning, and metering',
|
|
24
|
+
gems: %w[lex-tasker lex-scheduler lex-lex lex-conditioner lex-transformer lex-health lex-metering]
|
|
25
|
+
},
|
|
26
|
+
cognitive: {
|
|
27
|
+
label: 'Cognitive / Agentic',
|
|
28
|
+
description: 'Full GAIA cognitive stack (13 agentic domains + tick + mesh + apollo)',
|
|
29
|
+
gems: %w[legion-gaia]
|
|
30
|
+
},
|
|
31
|
+
ai: {
|
|
32
|
+
label: 'AI / LLM',
|
|
33
|
+
description: 'LLM routing, provider integration, and MCP tools',
|
|
34
|
+
gems: %w[legion-llm legion-mcp]
|
|
35
|
+
},
|
|
36
|
+
observability: {
|
|
37
|
+
label: 'Observability',
|
|
38
|
+
description: 'Telemetry, logging, anomaly detection, and webhooks',
|
|
39
|
+
gems: %w[lex-telemetry lex-log lex-webhook lex-detect]
|
|
40
|
+
},
|
|
41
|
+
governance: {
|
|
42
|
+
label: 'Governance & Security',
|
|
43
|
+
description: 'RBAC, audit trails, FinOps, PII protection, and lifecycle governance',
|
|
44
|
+
gems: %w[lex-governance lex-audit lex-finops lex-privatecore]
|
|
45
|
+
},
|
|
46
|
+
channels: {
|
|
47
|
+
label: 'Chat Channels',
|
|
48
|
+
description: 'Slack, Microsoft Teams, and GitHub chat adapters',
|
|
49
|
+
gems: %w[lex-slack lex-microsoft_teams lex-github]
|
|
50
|
+
},
|
|
51
|
+
devtools: {
|
|
52
|
+
label: 'Development Tools',
|
|
53
|
+
description: 'Eval gating, datasets, prompt templates, autofix, and mind-growth',
|
|
54
|
+
gems: %w[lex-eval lex-dataset lex-prompt lex-autofix lex-mind-growth]
|
|
55
|
+
},
|
|
56
|
+
swarm: {
|
|
57
|
+
label: 'Swarm / Multi-Agent',
|
|
58
|
+
description: 'Multi-agent orchestration, GitHub swarm pipeline, and ACP adapter',
|
|
59
|
+
gems: %w[lex-swarm lex-swarm-github lex-adapter lex-acp]
|
|
60
|
+
},
|
|
61
|
+
services: {
|
|
62
|
+
label: 'Service Integrations',
|
|
63
|
+
description: 'HTTP, Vault, and Consul service connectors',
|
|
64
|
+
gems: %w[lex-http lex-vault lex-consul]
|
|
65
|
+
}
|
|
66
|
+
}.freeze
|
|
67
|
+
|
|
68
|
+
desc 'install', 'Interactively select and install feature bundles'
|
|
69
|
+
option :dry_run, type: :boolean, default: false, desc: 'Show what would be installed without installing'
|
|
70
|
+
option :all, type: :boolean, default: false, desc: 'Install all feature bundles'
|
|
71
|
+
def install
|
|
72
|
+
out = formatter
|
|
73
|
+
selected = options[:all] ? BUNDLES.keys : prompt_bundle_selection(out)
|
|
74
|
+
|
|
75
|
+
return out.error('No bundles selected') if selected.empty?
|
|
76
|
+
|
|
77
|
+
gems = resolve_gems(selected)
|
|
78
|
+
installed, missing = partition_gems(gems)
|
|
79
|
+
|
|
80
|
+
if missing.empty?
|
|
81
|
+
report_all_present(out, selected, installed)
|
|
82
|
+
elsif options[:dry_run]
|
|
83
|
+
report_dry_run(out, selected, installed, missing)
|
|
84
|
+
else
|
|
85
|
+
execute_install(out, selected, installed, missing)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
desc 'list', 'Show available feature bundles and their install status'
|
|
90
|
+
def list
|
|
91
|
+
out = formatter
|
|
92
|
+
statuses = bundle_statuses
|
|
93
|
+
|
|
94
|
+
if options[:json]
|
|
95
|
+
out.json(bundles: statuses)
|
|
96
|
+
else
|
|
97
|
+
out.header('Feature Bundles')
|
|
98
|
+
out.spacer
|
|
99
|
+
statuses.each { |s| print_bundle_status(out, s) }
|
|
100
|
+
out.spacer
|
|
101
|
+
installed_count = statuses.count { |s| s[:missing].empty? }
|
|
102
|
+
puts " #{installed_count} of #{statuses.size} bundle(s) fully installed"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
no_commands do
|
|
107
|
+
def formatter
|
|
108
|
+
@formatter ||= Output::Formatter.new(
|
|
109
|
+
json: options[:json],
|
|
110
|
+
color: !options[:no_color]
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def prompt_bundle_selection(out)
|
|
117
|
+
require 'tty-prompt'
|
|
118
|
+
prompt = ::TTY::Prompt.new
|
|
119
|
+
statuses = bundle_statuses
|
|
120
|
+
|
|
121
|
+
choices = statuses.map do |s|
|
|
122
|
+
icon = s[:missing].empty? ? '(installed)' : "(#{s[:missing].size} gem(s) to install)"
|
|
123
|
+
{ name: "#{s[:label]} #{icon} - #{s[:description]}", value: s[:name] }
|
|
124
|
+
end
|
|
125
|
+
choices << { name: 'Everything - install all bundles above', value: :everything }
|
|
126
|
+
|
|
127
|
+
out.header('Legion Feature Bundles')
|
|
128
|
+
out.spacer
|
|
129
|
+
|
|
130
|
+
selected = prompt.multi_select('Select bundles to install:', choices, per_page: 12,
|
|
131
|
+
echo: false,
|
|
132
|
+
min: 1)
|
|
133
|
+
return BUNDLES.keys if selected.include?(:everything)
|
|
134
|
+
|
|
135
|
+
selected
|
|
136
|
+
rescue ::TTY::Reader::InputInterrupt, Interrupt
|
|
137
|
+
out.spacer
|
|
138
|
+
puts ' Cancelled.'
|
|
139
|
+
[]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def resolve_gems(bundle_keys)
|
|
143
|
+
bundle_keys.flat_map { |key| BUNDLES[key][:gems] }.uniq.sort
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def partition_gems(gem_names)
|
|
147
|
+
installed = []
|
|
148
|
+
missing = []
|
|
149
|
+
gem_names.each do |name|
|
|
150
|
+
Gem::Specification.find_by_name(name)
|
|
151
|
+
installed << name
|
|
152
|
+
rescue Gem::MissingSpecError
|
|
153
|
+
missing << name
|
|
154
|
+
end
|
|
155
|
+
[installed, missing]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def gem_version(name)
|
|
159
|
+
Gem::Specification.find_by_name(name).version.to_s
|
|
160
|
+
rescue Gem::MissingSpecError
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def bundle_statuses
|
|
165
|
+
BUNDLES.map do |name, bundle|
|
|
166
|
+
installed, missing = partition_gems(bundle[:gems])
|
|
167
|
+
{
|
|
168
|
+
name: name,
|
|
169
|
+
label: bundle[:label],
|
|
170
|
+
description: bundle[:description],
|
|
171
|
+
installed: installed.map { |g| { name: g, version: gem_version(g) } },
|
|
172
|
+
missing: missing
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def print_bundle_status(out, status)
|
|
178
|
+
icon = if status[:missing].empty?
|
|
179
|
+
out.colorize('installed', :success)
|
|
180
|
+
else
|
|
181
|
+
out.colorize("#{status[:missing].size} missing", :muted)
|
|
182
|
+
end
|
|
183
|
+
puts " #{out.colorize(status[:label].ljust(24), :label)} #{icon}"
|
|
184
|
+
status[:installed].each do |g|
|
|
185
|
+
puts " #{out.colorize(g[:name], :success)} #{g[:version]}"
|
|
186
|
+
end
|
|
187
|
+
status[:missing].each do |g|
|
|
188
|
+
puts " #{out.colorize(g, :muted)} (not installed)"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def report_all_present(out, selected, installed)
|
|
193
|
+
labels = selected.map { |k| BUNDLES[k][:label] }.join(', ')
|
|
194
|
+
if options[:json]
|
|
195
|
+
out.json(status: 'already_installed', bundles: selected,
|
|
196
|
+
gems: installed.map { |g| { name: g, version: gem_version(g) } })
|
|
197
|
+
else
|
|
198
|
+
out.success("All gems already installed for: #{labels}")
|
|
199
|
+
installed.each { |g| puts " #{g} #{gem_version(g)}" }
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def report_dry_run(out, selected, installed, missing)
|
|
204
|
+
labels = selected.map { |k| BUNDLES[k][:label] }.join(', ')
|
|
205
|
+
if options[:json]
|
|
206
|
+
out.json(status: 'dry_run', bundles: selected, to_install: missing,
|
|
207
|
+
already_installed: installed.map { |g| { name: g, version: gem_version(g) } })
|
|
208
|
+
else
|
|
209
|
+
out.header("Feature install dry run: #{labels}")
|
|
210
|
+
out.spacer
|
|
211
|
+
missing.each { |g| puts " #{out.colorize('install', :accent)} #{g}" }
|
|
212
|
+
installed.each { |g| puts " #{out.colorize('skip', :muted)} #{g} #{gem_version(g)} (already installed)" }
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def execute_install(out, selected, installed, missing)
|
|
217
|
+
labels = selected.map { |k| BUNDLES[k][:label] }.join(', ')
|
|
218
|
+
out.header("Installing: #{labels}") unless options[:json]
|
|
219
|
+
out.spacer unless options[:json]
|
|
220
|
+
puts " #{missing.size} gem(s) to install, #{installed.size} already present" unless options[:json]
|
|
221
|
+
out.spacer unless options[:json]
|
|
222
|
+
|
|
223
|
+
gem_bin = File.join(RbConfig::CONFIG['bindir'], 'gem')
|
|
224
|
+
results = missing.map { |g| install_gem(g, gem_bin, out) }
|
|
225
|
+
|
|
226
|
+
Gem::Specification.reset
|
|
227
|
+
successes, failures = results.partition { |r| r[:status] == 'installed' }
|
|
228
|
+
|
|
229
|
+
if options[:json]
|
|
230
|
+
out.json(bundles: selected, installed: successes, failed: failures,
|
|
231
|
+
already_present: installed.map { |g| { name: g, version: gem_version(g) } })
|
|
232
|
+
else
|
|
233
|
+
out.spacer
|
|
234
|
+
if failures.empty?
|
|
235
|
+
out.success("#{successes.size} gem(s) installed successfully")
|
|
236
|
+
else
|
|
237
|
+
out.error("#{failures.size} gem(s) failed to install")
|
|
238
|
+
failures.each { |f| puts " #{f[:name]}: #{f[:error]}" }
|
|
239
|
+
out.spacer
|
|
240
|
+
out.success("#{successes.size} gem(s) installed") unless successes.empty?
|
|
241
|
+
end
|
|
242
|
+
suggest_next_steps(out, selected)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def install_gem(name, gem_bin, out)
|
|
247
|
+
puts " Installing #{name}..." unless options[:json]
|
|
248
|
+
output = `#{gem_bin} install #{name} --no-document 2>&1`
|
|
249
|
+
if $CHILD_STATUS.success?
|
|
250
|
+
out.success(" #{name} installed") unless options[:json]
|
|
251
|
+
{ name: name, status: 'installed' }
|
|
252
|
+
else
|
|
253
|
+
out.error(" #{name} failed") unless options[:json]
|
|
254
|
+
{ name: name, status: 'failed', error: output.strip.lines.last&.strip }
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def suggest_next_steps(out, selected)
|
|
259
|
+
out.spacer
|
|
260
|
+
puts ' Next steps:'
|
|
261
|
+
if selected.include?(:cognitive) || selected.include?(:ai)
|
|
262
|
+
puts ' legion start # full daemon with cognitive stack'
|
|
263
|
+
puts ' legion start --lite # single-process, no external services'
|
|
264
|
+
puts ' legion chat # interactive AI conversation'
|
|
265
|
+
end
|
|
266
|
+
puts ' legion features list # verify installed bundles'
|
|
267
|
+
puts ' legion doctor # check environment health'
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
data/lib/legion/cli.rb
CHANGED
|
@@ -61,6 +61,7 @@ module Legion
|
|
|
61
61
|
autoload :Failover, 'legion/cli/failover_command'
|
|
62
62
|
autoload :Apollo, 'legion/cli/apollo_command'
|
|
63
63
|
autoload :TraceCommand, 'legion/cli/trace_command'
|
|
64
|
+
autoload :Features, 'legion/cli/features_command'
|
|
64
65
|
|
|
65
66
|
class Main < Thor
|
|
66
67
|
def self.exit_on_failure?
|
|
@@ -338,6 +339,9 @@ module Legion
|
|
|
338
339
|
desc 'trace SUBCOMMAND', 'Natural language trace search via LLM'
|
|
339
340
|
subcommand 'trace', Legion::CLI::TraceCommand
|
|
340
341
|
|
|
342
|
+
desc 'features SUBCOMMAND', 'Install feature bundles (interactive selector)'
|
|
343
|
+
subcommand 'features', Legion::CLI::Features
|
|
344
|
+
|
|
341
345
|
desc 'tree', 'Print a tree of all available commands'
|
|
342
346
|
def tree
|
|
343
347
|
legion_print_command_tree(self.class, 'legion', '')
|
|
@@ -86,6 +86,10 @@ module Legion
|
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def activate
|
|
89
|
+
unless @consumer
|
|
90
|
+
log.warn "[Subscription] skipping activate for #{lex_name}/#{runner_name}: no consumer (prepare failed?)"
|
|
91
|
+
return
|
|
92
|
+
end
|
|
89
93
|
@queue.subscribe_with(@consumer)
|
|
90
94
|
log.info "[Subscription] activated: #{lex_name}/#{runner_name} (consumer registered)"
|
|
91
95
|
end
|
|
@@ -96,8 +96,13 @@ module Legion
|
|
|
96
96
|
|
|
97
97
|
def full_path
|
|
98
98
|
@full_path ||= begin
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
base_name = segments.join('-')
|
|
100
|
+
gem_name = "lex-#{base_name}"
|
|
101
|
+
gem_dir = begin
|
|
102
|
+
Gem::Specification.find_by_name(gem_name).gem_dir
|
|
103
|
+
rescue Gem::MissingSpecError
|
|
104
|
+
Gem::Specification.find_by_name("lex-#{base_name.tr('_', '-')}").gem_dir
|
|
105
|
+
end
|
|
101
106
|
require_path = Helpers::Segments.derive_require_path(gem_name)
|
|
102
107
|
"#{gem_dir}/lib/#{require_path}"
|
|
103
108
|
end
|
|
@@ -106,12 +106,12 @@ module Legion
|
|
|
106
106
|
|
|
107
107
|
def bind_e_to_q(to:, from: default_exchange, routing_key: nil, **)
|
|
108
108
|
if from.is_a? String
|
|
109
|
-
from = "#{transport_class}::Exchanges::#{from.split('_').collect(&:capitalize).join}" unless from.include?('::')
|
|
109
|
+
from = "#{transport_class}::Exchanges::#{from.tr('.', '_').split('_').collect(&:capitalize).join}" unless from.include?('::')
|
|
110
110
|
auto_create_exchange(from) unless Object.const_defined? from
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
if to.is_a? String
|
|
114
|
-
to = "#{transport_class}::Queues::#{to.split('_').collect(&:capitalize).join}" unless to.include?('::')
|
|
114
|
+
to = "#{transport_class}::Queues::#{to.tr('.', '_').split('_').collect(&:capitalize).join}" unless to.include?('::')
|
|
115
115
|
auto_create_queue(to) unless Object.const_defined?(to)
|
|
116
116
|
end
|
|
117
117
|
|
data/lib/legion/extensions.rb
CHANGED
|
@@ -21,6 +21,7 @@ module Legion
|
|
|
21
21
|
@subscription_tasks = []
|
|
22
22
|
@local_tasks = []
|
|
23
23
|
@actors = []
|
|
24
|
+
@running_instances = Concurrent::Array.new
|
|
24
25
|
@pending_actors = Concurrent::Array.new
|
|
25
26
|
|
|
26
27
|
find_extensions
|
|
@@ -31,9 +32,12 @@ module Legion
|
|
|
31
32
|
|
|
32
33
|
attr_reader :local_tasks
|
|
33
34
|
|
|
34
|
-
def shutdown # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
35
|
+
def shutdown # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
35
36
|
return nil if @loaded_extensions.nil?
|
|
36
37
|
|
|
38
|
+
deadline = Legion::Settings.dig(:extensions, :shutdown_timeout) || 15
|
|
39
|
+
shutdown_start = Time.now
|
|
40
|
+
|
|
37
41
|
@loaded_extensions.each { |name| Catalog.transition(name, :stopping) }
|
|
38
42
|
|
|
39
43
|
if @subscription_pool
|
|
@@ -42,22 +46,54 @@ module Legion
|
|
|
42
46
|
@subscription_pool = nil
|
|
43
47
|
end
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
# Cancel all running instances (real objects, not new instances)
|
|
50
|
+
@running_instances&.each do |instance|
|
|
51
|
+
instance.cancel if instance.respond_to?(:cancel)
|
|
47
52
|
rescue StandardError => e
|
|
48
53
|
Legion::Logging.debug "Extension shutdown cancel failed: #{e.message}" if defined?(Legion::Logging)
|
|
49
54
|
end
|
|
50
55
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
# Wait for in-flight work to drain, up to deadline
|
|
57
|
+
remaining = deadline - (Time.now - shutdown_start)
|
|
58
|
+
if remaining.positive?
|
|
59
|
+
drain_start = Time.now
|
|
60
|
+
loop do
|
|
61
|
+
elapsed = Time.now - drain_start
|
|
62
|
+
break if elapsed >= remaining
|
|
63
|
+
|
|
64
|
+
still_active = @running_instances&.any? do |inst|
|
|
65
|
+
(inst.respond_to?(:channel) && inst.instance_variable_get(:@queue)&.channel&.open?) ||
|
|
66
|
+
(inst.instance_variable_get(:@timer).respond_to?(:running?) && inst.instance_variable_get(:@timer).running?) ||
|
|
67
|
+
(inst.instance_variable_get(:@loop) == true)
|
|
68
|
+
end
|
|
69
|
+
break unless still_active
|
|
70
|
+
|
|
71
|
+
sleep 0.25
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Force-close any channels still open after deadline
|
|
76
|
+
elapsed = Time.now - shutdown_start
|
|
77
|
+
if elapsed >= deadline
|
|
78
|
+
Legion::Logging.warn "Shutdown deadline (#{deadline}s) reached, force-closing remaining actors" if defined?(Legion::Logging)
|
|
79
|
+
@running_instances&.each do |inst|
|
|
80
|
+
queue = inst.instance_variable_get(:@queue)
|
|
81
|
+
queue&.channel&.close if queue&.channel.respond_to?(:close) && queue.channel.open?
|
|
82
|
+
timer = inst.instance_variable_get(:@timer)
|
|
83
|
+
timer&.kill if timer.respond_to?(:kill)
|
|
84
|
+
inst.instance_variable_set(:@loop, false) if inst.instance_variable_defined?(:@loop)
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
Legion::Logging.debug "Force-close failed: #{e.message}" if defined?(Legion::Logging)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
@running_instances&.clear
|
|
55
91
|
|
|
56
92
|
@loaded_extensions.each do |name|
|
|
57
93
|
Catalog.transition(name, :stopped)
|
|
58
94
|
unregister_capabilities(name)
|
|
59
95
|
end
|
|
60
|
-
Legion::Logging.info
|
|
96
|
+
Legion::Logging.info "Successfully shut down all actors (#{(Time.now - shutdown_start).round(1)}s)"
|
|
61
97
|
end
|
|
62
98
|
|
|
63
99
|
def load_extensions
|
|
@@ -276,12 +312,16 @@ module Legion
|
|
|
276
312
|
|
|
277
313
|
if actor_class.ancestors.include? Legion::Extensions::Actors::Every
|
|
278
314
|
@timer_tasks.push(extension_hash)
|
|
315
|
+
@running_instances << extension_hash[:running_class]
|
|
279
316
|
elsif actor_class.ancestors.include? Legion::Extensions::Actors::Once
|
|
280
317
|
@once_tasks.push(extension_hash)
|
|
318
|
+
@running_instances << extension_hash[:running_class]
|
|
281
319
|
elsif actor_class.ancestors.include? Legion::Extensions::Actors::Loop
|
|
282
320
|
@loop_tasks.push(extension_hash)
|
|
321
|
+
@running_instances << extension_hash[:running_class]
|
|
283
322
|
elsif actor_class.ancestors.include? Legion::Extensions::Actors::Poll
|
|
284
323
|
@poll_tasks.push(extension_hash)
|
|
324
|
+
@running_instances << extension_hash[:running_class]
|
|
285
325
|
elsif actor_class.ancestors.include? Legion::Extensions::Actors::Subscription
|
|
286
326
|
hook_subscription_actors_pooled([extension_hash])
|
|
287
327
|
else
|
|
@@ -368,6 +408,7 @@ module Legion
|
|
|
368
408
|
|
|
369
409
|
begin
|
|
370
410
|
entry[:instance].activate if entry[:instance].respond_to?(:activate)
|
|
411
|
+
@running_instances << entry[:instance]
|
|
371
412
|
rescue StandardError => e
|
|
372
413
|
ext_name = entry[:actor_hash][:extension_name]
|
|
373
414
|
Legion::Logging.error "[Subscription] activate failed for #{ext_name}: #{e.message}" if defined?(Legion::Logging)
|
|
@@ -709,8 +750,11 @@ module Legion
|
|
|
709
750
|
segments = Helpers::Segments.derive_segments(gem_name)
|
|
710
751
|
tier = category == :default ? 5 : (categories.dig(category, :tier) || 5)
|
|
711
752
|
|
|
712
|
-
# Multi-segment gem names
|
|
753
|
+
# Multi-segment gem names: check if the gem actually uses nested directories
|
|
754
|
+
# (e.g. lex-agentic-memory -> agentic/memory/) or flat underscored naming
|
|
755
|
+
# (e.g. lex-swarm-github -> swarm_github.rb). Probe the gem's lib/ to decide.
|
|
713
756
|
nesting = true if segments.length > 1
|
|
757
|
+
nesting = probe_nesting(gem_name, segments) if nesting && segments.length > 1
|
|
714
758
|
|
|
715
759
|
if nesting
|
|
716
760
|
const_path = Helpers::Segments.derive_const_path(gem_name)
|
|
@@ -725,6 +769,19 @@ module Legion
|
|
|
725
769
|
segments: segments, const_path: const_path, require_path: require_path }
|
|
726
770
|
end
|
|
727
771
|
|
|
772
|
+
def probe_nesting(gem_name, segments)
|
|
773
|
+
gem_dir = Gem::Specification.find_by_name(gem_name).gem_dir
|
|
774
|
+
nested_path = "#{gem_dir}/lib/legion/extensions/#{segments.join('/')}.rb"
|
|
775
|
+
return true if File.exist?(nested_path)
|
|
776
|
+
|
|
777
|
+
flat_path = "#{gem_dir}/lib/legion/extensions/#{segments.join('_')}.rb"
|
|
778
|
+
return false if File.exist?(flat_path)
|
|
779
|
+
|
|
780
|
+
true # default to nested if neither found
|
|
781
|
+
rescue Gem::MissingSpecError
|
|
782
|
+
true
|
|
783
|
+
end
|
|
784
|
+
|
|
728
785
|
def default_category_registry
|
|
729
786
|
{
|
|
730
787
|
core: { type: :list, tier: 1 },
|
data/lib/legion/service.rb
CHANGED
|
@@ -319,6 +319,7 @@ module Legion
|
|
|
319
319
|
def register_logging_hooks
|
|
320
320
|
return unless defined?(Legion::Transport::Connection)
|
|
321
321
|
return unless Legion::Transport::Connection.session_open?
|
|
322
|
+
return unless Legion::Transport::Connection.respond_to?(:log_channel)
|
|
322
323
|
|
|
323
324
|
log_ch = Legion::Transport::Connection.log_channel
|
|
324
325
|
unless log_ch
|
|
@@ -327,7 +328,7 @@ module Legion
|
|
|
327
328
|
end
|
|
328
329
|
|
|
329
330
|
require 'legion/transport/exchanges/logging' unless defined?(Legion::Transport::Exchanges::Logging)
|
|
330
|
-
exchange = Legion::Transport::Exchanges::Logging.new(channel: log_ch)
|
|
331
|
+
exchange = Legion::Transport::Exchanges::Logging.new('legion.logging', channel: log_ch)
|
|
331
332
|
|
|
332
333
|
%i[fatal error warn].each do |level|
|
|
333
334
|
Legion::Logging.send(:"on_#{level}") do |event|
|
data/lib/legion/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legionio
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.5.
|
|
4
|
+
version: 1.5.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -225,28 +225,28 @@ dependencies:
|
|
|
225
225
|
requirements:
|
|
226
226
|
- - ">="
|
|
227
227
|
- !ruby/object:Gem::Version
|
|
228
|
-
version: 1.3.
|
|
228
|
+
version: 1.3.16
|
|
229
229
|
type: :runtime
|
|
230
230
|
prerelease: false
|
|
231
231
|
version_requirements: !ruby/object:Gem::Requirement
|
|
232
232
|
requirements:
|
|
233
233
|
- - ">="
|
|
234
234
|
- !ruby/object:Gem::Version
|
|
235
|
-
version: 1.3.
|
|
235
|
+
version: 1.3.16
|
|
236
236
|
- !ruby/object:Gem::Dependency
|
|
237
237
|
name: legion-crypt
|
|
238
238
|
requirement: !ruby/object:Gem::Requirement
|
|
239
239
|
requirements:
|
|
240
240
|
- - ">="
|
|
241
241
|
- !ruby/object:Gem::Version
|
|
242
|
-
version: 1.4.
|
|
242
|
+
version: 1.4.12
|
|
243
243
|
type: :runtime
|
|
244
244
|
prerelease: false
|
|
245
245
|
version_requirements: !ruby/object:Gem::Requirement
|
|
246
246
|
requirements:
|
|
247
247
|
- - ">="
|
|
248
248
|
- !ruby/object:Gem::Version
|
|
249
|
-
version: 1.4.
|
|
249
|
+
version: 1.4.12
|
|
250
250
|
- !ruby/object:Gem::Dependency
|
|
251
251
|
name: legion-data
|
|
252
252
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -295,28 +295,28 @@ dependencies:
|
|
|
295
295
|
requirements:
|
|
296
296
|
- - ">="
|
|
297
297
|
- !ruby/object:Gem::Version
|
|
298
|
-
version: 1.3.
|
|
298
|
+
version: 1.3.19
|
|
299
299
|
type: :runtime
|
|
300
300
|
prerelease: false
|
|
301
301
|
version_requirements: !ruby/object:Gem::Requirement
|
|
302
302
|
requirements:
|
|
303
303
|
- - ">="
|
|
304
304
|
- !ruby/object:Gem::Version
|
|
305
|
-
version: 1.3.
|
|
305
|
+
version: 1.3.19
|
|
306
306
|
- !ruby/object:Gem::Dependency
|
|
307
307
|
name: legion-transport
|
|
308
308
|
requirement: !ruby/object:Gem::Requirement
|
|
309
309
|
requirements:
|
|
310
310
|
- - ">="
|
|
311
311
|
- !ruby/object:Gem::Version
|
|
312
|
-
version: 1.
|
|
312
|
+
version: 1.4.0
|
|
313
313
|
type: :runtime
|
|
314
314
|
prerelease: false
|
|
315
315
|
version_requirements: !ruby/object:Gem::Requirement
|
|
316
316
|
requirements:
|
|
317
317
|
- - ">="
|
|
318
318
|
- !ruby/object:Gem::Version
|
|
319
|
-
version: 1.
|
|
319
|
+
version: 1.4.0
|
|
320
320
|
- !ruby/object:Gem::Dependency
|
|
321
321
|
name: legion-tty
|
|
322
322
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -576,6 +576,7 @@ files:
|
|
|
576
576
|
- lib/legion/cli/error_handler.rb
|
|
577
577
|
- lib/legion/cli/eval_command.rb
|
|
578
578
|
- lib/legion/cli/failover_command.rb
|
|
579
|
+
- lib/legion/cli/features_command.rb
|
|
579
580
|
- lib/legion/cli/function.rb
|
|
580
581
|
- lib/legion/cli/gaia_command.rb
|
|
581
582
|
- lib/legion/cli/generate_command.rb
|