legionio 1.4.8 → 1.4.10

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: 6f04739022a1b0f58b820dbcc6be1850a1830cd852b6b253c9069d9f9eba5bb1
4
- data.tar.gz: afa47da3a0fabe43c5379e6a21552fdf65270f6a5426f1309b41e7a390281d91
3
+ metadata.gz: 6daf36d8c0a062f8acf93959c47ce9b3135159dd4242967f52f6b938bec63e88
4
+ data.tar.gz: fd0b555351df2e91186743e2ec527576ec245f68730126354fafb348aa140bd8
5
5
  SHA512:
6
- metadata.gz: 89de4e81fc89e8610b7b512390d6864dff9cb152079d1ffd02f084d5afb5086ccdf45f78a5947a5f5a3206158768d77d8651df48644bd7c98ba04c77654d11b6
7
- data.tar.gz: b343fa58368cda584954ba83d85b55b64c2c7e64bbe5dbdcdc841ad75ae553781ddb05d22adcf98898345b4afe0a20d3c902e29a7e4fe7e558121f6c89f56d9f
6
+ metadata.gz: 3ed2e17fad085d4a5b5c2e57f77ef5cba0c52f6454b375d8b50aa36d6a2a5ceebefeb002de9cd835edcffc6e9dd7832c43d400c5a422d0383572ec92657a3980
7
+ data.tar.gz: d68a39c49e4c24c3c0caae1278a922a42552894945afa665758c08fd30363db24b01f0dfb9c93e57ecc1e5110a88db0a820b0aa6fa61782e794e22cee3828074
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.4.10] - 2026-03-16
4
+
5
+ ### Fixed
6
+ - API startup no longer crashes when port is already in use (rolling restart support)
7
+ - `setup_api` retries binding up to 10 times with 3s wait (configurable via `api.bind_retries` and `api.bind_retry_wait`)
8
+ - Port bind failure after retries marks API as not-ready instead of killing the thread
9
+
10
+ ## [1.4.9] - 2026-03-16
11
+
12
+ ### Added
13
+ - YJIT enabled at process start for 15-30% runtime throughput improvement (Ruby 3.1+ builds)
14
+ - GC tuning ENV defaults for large gem count workloads (overridable via environment)
15
+ - bootsnap bytecode and load-path caching at `~/.legionio/cache/bootsnap/`
16
+ - Role-based extension profiles: nil (all), core, cognitive, service, dev, custom
17
+ - Extension discovery uses Bundler specs when available for faster boot
18
+
19
+ ### Changed
20
+ - `find_extensions` uses `Bundler.load.specs` instead of `Gem::Specification.all_names` under Bundler
21
+ - `lex-` prefix check uses `start_with?` instead of string slicing
22
+
3
23
  ## v1.4.8
4
24
 
5
25
  ### Fixed
data/CLAUDE.md CHANGED
@@ -9,13 +9,21 @@ The primary gem for the LegionIO framework. An extensible async job engine for s
9
9
 
10
10
  **GitHub**: https://github.com/LegionIO/LegionIO
11
11
  **Gem**: `legionio`
12
- **Version**: 1.4.8
12
+ **Version**: 1.4.9
13
13
  **License**: Apache-2.0
14
14
  **Docker**: `legionio/legion`
15
15
  **Ruby**: >= 3.4
16
16
 
17
17
  ## Architecture
18
18
 
19
+ ### Boot Sequence (exe/legion)
20
+
21
+ Before any Legion code loads, `exe/legion` applies three performance optimizations:
22
+
23
+ 1. **YJIT** — `RubyVM::YJIT.enable` for 15-30% runtime throughput (guarded with `if defined?`)
24
+ 2. **GC tuning** — pre-allocates 600k heap slots, raises malloc limits (all `||=` so ENV overrides are respected)
25
+ 3. **bootsnap** — caches YARV bytecodes and `$LOAD_PATH` resolution at `~/.legionio/cache/bootsnap/`
26
+
19
27
  ### Startup Sequence
20
28
 
21
29
  ```
@@ -29,7 +37,7 @@ Legion.start
29
37
  ├── 6. setup_data (legion-data, MySQL/SQLite + migrations, optional)
30
38
  ├── 7. setup_llm (legion-llm, optional)
31
39
  ├── 8. setup_supervision (process supervision)
32
- ├── 9. load_extensions (discover + load LEX gems)
40
+ ├── 9. load_extensions (discover + load LEX gems, filtered by role profile)
33
41
  ├── 10. Legion::Crypt.cs (distribute cluster secret)
34
42
  └── 11. setup_api (start Sinatra/Puma on port 4567)
35
43
  ```
@@ -192,7 +200,20 @@ Legion (lib/legion.rb)
192
200
 
193
201
  ### Extension Discovery
194
202
 
195
- `Legion::Extensions.find_extensions` scans `Gem::Specification.all_names` for gems starting with `lex-`. It also processes `Legion::Settings[:extensions]` for explicitly configured extensions, attempting `Gem.install` for missing ones if `auto_install` is enabled.
203
+ `Legion::Extensions.find_extensions` discovers lex-* gems via `Bundler.load.specs` (when running under Bundler) or falls back to `Gem::Specification.all_names`. It also processes `Legion::Settings[:extensions]` for explicitly configured extensions, attempting `Gem.install` for missing ones if `auto_install` is enabled.
204
+
205
+ **Role-based filtering**: After discovery, `apply_role_filter` prunes extensions based on `Legion::Settings[:role][:profile]`:
206
+
207
+ | Profile | What loads |
208
+ |---------|-----------|
209
+ | `nil` (default) | Everything — no filtering |
210
+ | `:core` | 14 core operational extensions only |
211
+ | `:cognitive` | core + all agentic extensions |
212
+ | `:service` | core + service + other integrations |
213
+ | `:dev` | core + AI + essential agentic (~20 extensions) |
214
+ | `:custom` | only what's listed in `role[:extensions]` |
215
+
216
+ Configure via settings JSON: `{"role": {"profile": "dev"}}`
196
217
 
197
218
  Loader checks per extension:
198
219
  - `data_required?` — skipped if legion-data not connected
@@ -368,6 +389,7 @@ legion
368
389
  | `lex-node` | Node identity extension |
369
390
  | `concurrent-ruby` + `ext` (>= 1.2) | Thread pool, concurrency primitives |
370
391
  | `daemons` (>= 1.4) | Process daemonization |
392
+ | `bootsnap` (>= 1.18) | YARV bytecode + load-path caching |
371
393
  | `oj` (>= 3.16) | Fast JSON (C extension) |
372
394
  | `puma` (>= 6.0) | HTTP server for API |
373
395
  | `mcp` (~> 0.8) | MCP server SDK |
@@ -496,7 +518,7 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
496
518
  | `lib/legion/cli/relationship.rb` | Old relationship commands |
497
519
  | `lib/legion/cli/lex/` | Old LEX sub-generators + ERB templates (still used by LexGenerator) |
498
520
  | **Executables** | |
499
- | `exe/legion` | Only executable: `Legion::CLI::Main.start(ARGV)` |
521
+ | `exe/legion` | Executable: YJIT, GC tuning, bootsnap, then `Legion::CLI::Main.start(ARGV)` |
500
522
  | `Dockerfile` | Docker build |
501
523
  | `docker_deploy.rb` | Build + push Docker image |
502
524
  | **Specs** | |
@@ -522,7 +544,7 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
522
544
 
523
545
  ```bash
524
546
  bundle install
525
- bundle exec rspec # 872 examples, 0 failures
547
+ bundle exec rspec # 880 examples, 0 failures
526
548
  bundle exec rubocop # 0 offenses
527
549
  ```
528
550
 
data/exe/legion CHANGED
@@ -1,6 +1,23 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
5
+
6
+ ENV['RUBY_GC_HEAP_INIT_SLOTS'] ||= '600000'
7
+ ENV['RUBY_GC_HEAP_FREE_SLOTS_MIN_RATIO'] ||= '0.20'
8
+ ENV['RUBY_GC_HEAP_FREE_SLOTS_MAX_RATIO'] ||= '0.40'
9
+ ENV['RUBY_GC_MALLOC_LIMIT'] ||= '64000000'
10
+ ENV['RUBY_GC_MALLOC_LIMIT_MAX'] ||= '128000000'
11
+
12
+ require 'bootsnap'
13
+ Bootsnap.setup(
14
+ cache_dir: File.expand_path('~/.legionio/cache/bootsnap'),
15
+ development_mode: false,
16
+ load_path_cache: true,
17
+ compile_cache_iseq: true,
18
+ compile_cache_yaml: true
19
+ )
20
+
4
21
  $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
22
 
6
23
  require 'legion/cli'
data/legionio.gemspec CHANGED
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec|
36
36
 
37
37
  spec.add_dependency 'mcp', '~> 0.8'
38
38
 
39
+ spec.add_dependency 'bootsnap', '>= 1.18'
39
40
  spec.add_dependency 'concurrent-ruby', '>= 1.2'
40
41
  spec.add_dependency 'concurrent-ruby-ext', '>= 1.2'
41
42
  spec.add_dependency 'daemons', '>= 1.4'
@@ -208,10 +208,65 @@ module Legion
208
208
  false
209
209
  end
210
210
 
211
+ def gem_names_for_discovery
212
+ if defined?(Bundler)
213
+ Bundler.load.specs.map { |s| "#{s.name}-#{s.version}" }
214
+ else
215
+ Gem::Specification.all_names
216
+ end
217
+ end
218
+
219
+ def apply_role_filter
220
+ role = Legion::Settings[:role]
221
+ return if role.nil? || role[:profile].nil?
222
+
223
+ profile = role[:profile].to_sym
224
+ allowed = case profile
225
+ when :core then core_extension_names
226
+ when :cognitive then core_extension_names + agentic_extension_names
227
+ when :service then core_extension_names + service_extension_names + other_extension_names
228
+ when :dev then core_extension_names + ai_extension_names + dev_agentic_names
229
+ when :custom then Array(role[:extensions]).map(&:to_s)
230
+ else return
231
+ end
232
+
233
+ before = @extensions.count
234
+ @extensions.select! { |name, _| allowed.include?(name) }
235
+ Legion::Logging.info "Role profile :#{profile} filtered #{before} -> #{@extensions.count} extensions"
236
+ end
237
+
238
+ def core_extension_names
239
+ %w[codegen conditioner exec health lex log metering node ping scheduler tasker task_pruner telemetry
240
+ transformer].freeze
241
+ end
242
+
243
+ def ai_extension_names
244
+ %w[claude gemini openai].freeze
245
+ end
246
+
247
+ def service_extension_names
248
+ %w[consul github http microsoft_teams nomad redis s3 tfe vault].freeze
249
+ end
250
+
251
+ def other_extension_names
252
+ %w[chef elastic_app_search elasticsearch influxdb memcached pagerduty pushbullet pushover slack sleepiq smtp
253
+ sonos ssh todoist twilio].freeze
254
+ end
255
+
256
+ def dev_agentic_names
257
+ %w[attention coldstart curiosity dream empathy flow habit memory metacognition mood narrator personality
258
+ reflection salience temporal tick volition].freeze
259
+ end
260
+
261
+ def agentic_extension_names
262
+ known = core_extension_names + service_extension_names + other_extension_names + ai_extension_names
263
+ @extensions.keys.reject { |name| known.include?(name) }
264
+ end
265
+
211
266
  def find_extensions
212
267
  @extensions ||= {}
213
- Gem::Specification.all_names.each do |gem|
214
- next unless gem[0..3] == 'lex-'
268
+ gem_names_for_discovery.each do |gem|
269
+ next unless gem.start_with?('lex-')
215
270
 
216
271
  lex = gem.split('-')
217
272
  @extensions[lex[1]] = { full_gem_name: gem,
@@ -221,6 +276,8 @@ module Legion
221
276
  extension_class: "Legion::Extensions::#{lex[1].split('_').collect(&:capitalize).join}" }
222
277
  end
223
278
 
279
+ apply_role_filter
280
+
224
281
  enabled = 0
225
282
  requested = 0
226
283
 
@@ -122,12 +122,28 @@ module Legion
122
122
  bind = api_settings[:bind] || '0.0.0.0'
123
123
 
124
124
  @api_thread = Thread.new do
125
- Legion::API.set :port, port
126
- Legion::API.set :bind, bind
127
- Legion::API.set :server, :puma
128
- Legion::API.set :environment, :production
129
- Legion::Logging.info "Starting Legion API on #{bind}:#{port}"
130
- Legion::API.run!(traps: false)
125
+ retries = 0
126
+ max_retries = api_settings.fetch(:bind_retries, 10)
127
+ retry_wait = api_settings.fetch(:bind_retry_wait, 3)
128
+
129
+ begin
130
+ Legion::API.set :port, port
131
+ Legion::API.set :bind, bind
132
+ Legion::API.set :server, :puma
133
+ Legion::API.set :environment, :production
134
+ Legion::Logging.info "Starting Legion API on #{bind}:#{port}"
135
+ Legion::API.run!(traps: false)
136
+ rescue Errno::EADDRINUSE
137
+ retries += 1
138
+ if retries <= max_retries
139
+ Legion::Logging.warn "Port #{port} in use, retrying in #{retry_wait}s (attempt #{retries}/#{max_retries})"
140
+ sleep retry_wait
141
+ retry
142
+ else
143
+ Legion::Logging.error "Port #{port} still in use after #{max_retries} attempts, API disabled"
144
+ Legion::Readiness.mark_not_ready(:api)
145
+ end
146
+ end
131
147
  end
132
148
  Legion::Readiness.mark_ready(:api)
133
149
  rescue LoadError => e
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.4.8'
4
+ VERSION = '1.4.10'
5
5
  end
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.4.8
4
+ version: 1.4.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0.8'
26
+ - !ruby/object:Gem::Dependency
27
+ name: bootsnap
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '1.18'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '1.18'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: concurrent-ruby
28
42
  requirement: !ruby/object:Gem::Requirement