legionio 1.7.6 → 1.7.12

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -1
  3. data/CHANGELOG.md +39 -0
  4. data/legionio.gemspec +8 -8
  5. data/lib/legion/api/audit.rb +1 -1
  6. data/lib/legion/api/default_settings.rb +48 -0
  7. data/lib/legion/api/events.rb +38 -15
  8. data/lib/legion/api/helpers.rb +29 -7
  9. data/lib/legion/api/library_routes.rb +3 -0
  10. data/lib/legion/api/llm.rb +5 -2
  11. data/lib/legion/api/tenants.rb +5 -6
  12. data/lib/legion/api/workers.rb +1 -14
  13. data/lib/legion/api.rb +22 -2
  14. data/lib/legion/cli/chat/chat_logger.rb +19 -12
  15. data/lib/legion/cli/check_command.rb +4 -2
  16. data/lib/legion/cli/config_command.rb +1 -1
  17. data/lib/legion/cli/error_handler.rb +8 -2
  18. data/lib/legion/cli/start.rb +3 -2
  19. data/lib/legion/cli.rb +13 -2
  20. data/lib/legion/context.rb +18 -0
  21. data/lib/legion/extensions/actors/base.rb +6 -3
  22. data/lib/legion/extensions/actors/every.rb +4 -3
  23. data/lib/legion/extensions/actors/loop.rb +1 -1
  24. data/lib/legion/extensions/actors/poll.rb +4 -4
  25. data/lib/legion/extensions/actors/subscription.rb +12 -9
  26. data/lib/legion/extensions/catalog.rb +77 -11
  27. data/lib/legion/extensions/core.rb +24 -7
  28. data/lib/legion/extensions/helpers/logger.rb +3 -62
  29. data/lib/legion/extensions/helpers/secret.rb +2 -0
  30. data/lib/legion/extensions/helpers/task.rb +4 -2
  31. data/lib/legion/extensions/transport.rb +18 -4
  32. data/lib/legion/ingress.rb +12 -8
  33. data/lib/legion/region.rb +36 -1
  34. data/lib/legion/runner.rb +34 -19
  35. data/lib/legion/service.rb +194 -135
  36. data/lib/legion/task_outcome_observer.rb +32 -8
  37. data/lib/legion/telemetry.rb +19 -11
  38. data/lib/legion/version.rb +1 -1
  39. data/lib/legion/webhooks.rb +169 -40
  40. metadata +18 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30ac0f16cae1d42de41e7ed922e2fd75d96093017f7d562e2596b9c20bea99b6
4
- data.tar.gz: 23fe67ca7f6171ea6e0ed13d50b382e78e1a7df4402cf1fe8174fcfea7d92ceb
3
+ metadata.gz: 60c67de284b066264f07c3ad829b8d193b66d85ae900af468dd7a13165695da8
4
+ data.tar.gz: 8fd7ab97436343722619c6c77e0aaff9b6f664609db1b81a0daff97c6692f419
5
5
  SHA512:
6
- metadata.gz: ada4b04b1a274b66d60ab44ae9abe4b8ea998576d7d4a5b679ab3099ad91615924fe4646f671b544820d0b1db6e2bbcc0ceea84f2e5ed762e3f1321500017d99
7
- data.tar.gz: b1a5d03f53e03039321f54529b0eae7abdc82462a20ee0dd0c1cb88927b15f606a07472b69cdeacc488217fc941d62074f83e567c09d3d26f21c821328309f14
6
+ metadata.gz: 2fde83c468f409a35fc9e02d1dc39d969a0229d32c8b592162789683f21afa31df90763792a1c4c74cb74f13ef21f1288e125d78b2b68ac2f9d724a08eee1e77
7
+ data.tar.gz: d10be584f4e30e1f164545c5c6e72f2e3cfa627e19675021edaf18ee6224d3f66d92ce50fcaf44b40caf0e47515e61d137e921f3903e5cab34254245fcd84bfa
data/.rubocop.yml CHANGED
@@ -5,6 +5,8 @@ AllCops:
5
5
 
6
6
  Layout/LineLength:
7
7
  Max: 160
8
+ Exclude:
9
+ - 'Gemfile'
8
10
 
9
11
  Layout/SpaceAroundEqualsInParameterDefault:
10
12
  EnforcedStyle: space
@@ -65,7 +67,7 @@ Metrics/BlockLength:
65
67
  - 'lib/legion/cli/mode_command.rb'
66
68
 
67
69
  Metrics/AbcSize:
68
- Max: 60
70
+ Max: 62
69
71
  Exclude:
70
72
  - 'lib/legion/cli/chat_command.rb'
71
73
  - 'lib/legion/api/llm.rb'
data/CHANGELOG.md CHANGED
@@ -2,6 +2,45 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.7.12] - 2026-04-03
6
+
7
+ ### Fixed
8
+ - Fixes #110: normal daemon boot now prefers library-owned LLM and Apollo API routes, `/api/tenants` uses canonical JSON parsing with correct status codes, SSE listeners drain worker threads on disconnect, paginated collections avoid unconditional `COUNT(*)` unless explicitly requested, and service startup skips duplicate settings loads once configuration is already bootstrapped
9
+
10
+ ## [1.7.11] - 2026-04-02
11
+
12
+ ### Fixed
13
+ - Fixes #113: webhook deliveries now retry non-2xx responses and transport exceptions up to `max_retries`, record per-attempt delivery rows, dead-letter terminal failures, and cache active webhook pattern matching to reduce per-event dispatch overhead
14
+
15
+ ## [1.7.10] - 2026-04-02
16
+
17
+ ### Changed
18
+ - Bumped minimum dependency floors for Legion core gems, including `legion-logging >= 1.5.0`, `legion-settings >= 1.3.25`, and updated transport, data, cache, crypt, Apollo, and MCP minimums
19
+ - Stabilized the `LegionIO` spec suite by fixing the OAuth callback, catalog, and service shutdown regression specs
20
+ - CLI startup now honors settings-driven log levels, normalizes `start --help` into the standard Thor help flow, and routes chat/error logging through the newer helper-backed logger path
21
+ - `Legion::Service`, telemetry, and webhook runtime paths now use structured helper logging more consistently, respect configured logging when no CLI override is passed, and avoid brittle settings reads during boot
22
+ - Extension runtime wiring now deep-dups merged settings, lazily registers the local `extension_catalog` migration, publishes catalog transitions directly to transport, and surfaces auto-binding failures more clearly
23
+ - Secret, region, and task-outcome helpers now use canonical Vault connectivity checks, cache metadata misses more safely, and create meta-learning domains on demand before recording learning episodes
24
+
25
+ ## [1.7.8] - 2026-04-01
26
+
27
+ ### Added
28
+ - `Legion::API::Settings` module with registered defaults via `merge_settings('api', ...)`, matching the pattern used by all other LegionIO gems
29
+ - Puma `persistent_timeout` (20s) and `first_data_timeout` (30s) now configurable via `Settings[:api][:puma]`
30
+
31
+ ### Changed
32
+ - Removed all inline `||` and `.fetch(..., default)` fallbacks for API settings in `service.rb` and `check_command.rb` — defaults now guaranteed by `merge_settings`
33
+
34
+ ## [1.7.7] - 2026-04-01
35
+
36
+ ### Changed
37
+ - Integrated legion-logging 1.4.3 Helper refactor: all log output now uses structured segment tagging, colored exception output, and thread-local task context
38
+ - Slimmed `Extensions::Helpers::Logger` to thin override; `derive_component_type`, `lex_gem_name`, `gem_spec_for_lex`, `log_lex_name` now live in legion-logging gem
39
+ - Added `handle_runner_exception` for runner-specific exception handling (TaskLog publish + HandledTask raise)
40
+ - Added `Legion::Context.with_task_context` and `.current_task_context` for thread-local task propagation
41
+ - Wrapped all 5 dispatch paths (Runner.run, Subscription#dispatch_runner, Base#runner, Ingress local/remote) with context propagation
42
+ - Migrated 13 `log.log_exception` call sites to `handle_exception` across actors, core, transport, and task helpers
43
+
5
44
  ## [1.7.6] - 2026-04-01
6
45
 
7
46
  ### Changed
data/legionio.gemspec CHANGED
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.bindir = 'exe'
35
35
  spec.executables = %w[legion legionio]
36
36
 
37
- spec.add_dependency 'legion-mcp', '>= 0.5.1'
37
+ spec.add_dependency 'legion-mcp', '>= 0.7.1'
38
38
 
39
39
  spec.add_dependency 'kramdown', '>= 2.0'
40
40
 
@@ -52,15 +52,15 @@ 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.16'
56
- spec.add_dependency 'legion-crypt', '>= 1.4.17'
57
- spec.add_dependency 'legion-data', '>= 1.6.7'
55
+ spec.add_dependency 'legion-cache', '>= 1.3.21'
56
+ spec.add_dependency 'legion-crypt', '>= 1.5.0'
57
+ spec.add_dependency 'legion-data', '>= 1.6.19'
58
58
  spec.add_dependency 'legion-json', '>= 1.2.1'
59
- spec.add_dependency 'legion-logging', '>= 1.4.0'
60
- spec.add_dependency 'legion-settings', '>= 1.3.19'
61
- spec.add_dependency 'legion-transport', '>= 1.4.4'
59
+ spec.add_dependency 'legion-logging', '>= 1.5.0'
60
+ spec.add_dependency 'legion-settings', '>= 1.3.25'
61
+ spec.add_dependency 'legion-transport', '>= 1.4.13'
62
62
 
63
- spec.add_dependency 'legion-apollo', '>= 0.3.1'
63
+ spec.add_dependency 'legion-apollo', '>= 0.4.0'
64
64
  spec.add_dependency 'legion-gaia', '>= 0.9.26'
65
65
  spec.add_dependency 'legion-llm', '>= 0.5.8'
66
66
  spec.add_dependency 'legion-tty', '>= 0.4.35'
@@ -4,7 +4,7 @@ module Legion
4
4
  class API < Sinatra::Base
5
5
  module Routes
6
6
  module Audit
7
- def self.registered(app) # rubocop:disable Metrics/AbcSize
7
+ def self.registered(app)
8
8
  app.get '/api/audit' do
9
9
  require_data!
10
10
  dataset = Legion::Data::Model::AuditLog.order(Sequel.desc(:id))
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+
5
+ module Legion
6
+ class API < Sinatra::Base
7
+ module Settings
8
+ def self.default
9
+ {
10
+ enabled: true,
11
+ port: 4567,
12
+ bind: '0.0.0.0',
13
+ puma: puma_defaults,
14
+ bind_retries: 3,
15
+ bind_retry_wait: 2,
16
+ tls: tls_defaults
17
+ }
18
+ end
19
+
20
+ def self.puma_defaults
21
+ {
22
+ min_threads: 10,
23
+ max_threads: 16,
24
+ persistent_timeout: 20,
25
+ first_data_timeout: 30
26
+ }
27
+ end
28
+
29
+ def self.tls_defaults
30
+ {
31
+ enabled: false
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ begin
39
+ Legion::Settings.merge_settings('api', Legion::API::Settings.default) if Legion.const_defined?('Settings', false)
40
+ rescue StandardError => e
41
+ if Legion.const_defined?('Logging', false) && Legion::Logging.respond_to?(:fatal)
42
+ Legion::Logging.fatal(e.message)
43
+ Legion::Logging.fatal(e.backtrace)
44
+ else
45
+ puts e.message
46
+ puts e.backtrace
47
+ end
48
+ end
@@ -5,6 +5,7 @@ module Legion
5
5
  module Routes
6
6
  module Events
7
7
  BUFFER_SIZE = 100
8
+ SSE_STOP = Object.new.freeze
8
9
 
9
10
  class << self
10
11
  def event_buffer
@@ -38,6 +39,42 @@ module Legion
38
39
  @listener_installed = true
39
40
  end
40
41
 
42
+ def write_sse_event(out, event)
43
+ payload = event.transform_keys(&:to_s)
44
+ out << "event: #{payload['event']}\ndata: #{Legion::JSON.dump(payload)}\n\n"
45
+ end
46
+
47
+ def stop_queue_stream(queue:, worker:, listener:)
48
+ Legion::Events.off('*', listener) if defined?(Legion::Events)
49
+ return unless worker&.alive?
50
+
51
+ queue.push(SSE_STOP)
52
+ worker.join(0.1)
53
+ rescue ThreadError, IOError, Errno::EPIPE => e
54
+ Legion::Logging.debug("Events SSE cleanup failed: #{e.message}") if defined?(Legion::Logging)
55
+ end
56
+
57
+ def stream_queue(out:, queue:, listener:)
58
+ worker = Thread.new do
59
+ loop do
60
+ event = queue.pop
61
+ break if event.equal?(SSE_STOP)
62
+
63
+ write_sse_event(out, event)
64
+ rescue IOError, Errno::EPIPE => e
65
+ Legion::Logging.debug("Events SSE stream broken for #{event[:event]}: #{e.message}") if defined?(Legion::Logging)
66
+ break
67
+ end
68
+ ensure
69
+ Legion::Events.off('*', listener) if defined?(Legion::Events)
70
+ end
71
+
72
+ cleanup = proc { stop_queue_stream(queue: queue, worker: worker, listener: listener) }
73
+ out.callback(&cleanup)
74
+ out.errback(&cleanup)
75
+ worker
76
+ end
77
+
41
78
  def registered(app)
42
79
  install_listener if defined?(Legion::Events)
43
80
 
@@ -53,21 +90,7 @@ module Legion
53
90
  end
54
91
 
55
92
  stream do |out|
56
- Thread.new do
57
- loop do
58
- event = queue.pop
59
- data = Legion::JSON.dump(event.transform_keys(&:to_s))
60
- out << "event: #{event[:event]}\ndata: #{data}\n\n"
61
- rescue IOError, Errno::EPIPE => e
62
- Legion::Logging.debug "Events SSE stream broken for #{event[:event]}: #{e.message}" if defined?(Legion::Logging)
63
- break
64
- end
65
- ensure
66
- Legion::Events.off('*', listener)
67
- end
68
-
69
- out.callback { Legion::Events.off('*', listener) }
70
- out.errback { Legion::Events.off('*', listener) }
93
+ stream_queue(out: out, queue: queue, listener: listener)
71
94
  end
72
95
  end
73
96
 
@@ -16,17 +16,20 @@ module Legion
16
16
  content_type :json
17
17
  status status_code
18
18
 
19
- total = dataset.respond_to?(:count) ? dataset.count : dataset.length
20
19
  paginated = paginate(dataset)
21
- items = paginated.respond_to?(:all) ? paginated.all : paginated
20
+ items = paginated.respond_to?(:all) ? paginated.all : Array(paginated)
21
+ total = collection_total(dataset, items)
22
+ meta = response_meta.merge(
23
+ count: items.length,
24
+ limit: page_limit,
25
+ offset: page_offset
26
+ )
27
+ meta[:total] = total unless total.nil?
28
+ meta[:has_more] = collection_has_more?(items, total)
22
29
 
23
30
  Legion::JSON.dump({
24
31
  data: items.map { |r| r.respond_to?(:values) ? r.values : r },
25
- meta: response_meta.merge(
26
- total: total,
27
- limit: page_limit,
28
- offset: page_offset
29
- )
32
+ meta: meta
30
33
  })
31
34
  end
32
35
 
@@ -198,6 +201,25 @@ module Legion
198
201
  dataset
199
202
  end
200
203
  end
204
+
205
+ def include_total_count?
206
+ params[:include_total].to_s == 'true'
207
+ end
208
+
209
+ def collection_total(dataset, items)
210
+ return dataset.count if include_total_count? && dataset.respond_to?(:count)
211
+ return dataset.length if dataset.respond_to?(:length) && !dataset.respond_to?(:limit)
212
+
213
+ return page_offset + items.length if items.length < page_limit
214
+
215
+ nil
216
+ end
217
+
218
+ def collection_has_more?(items, total)
219
+ return (page_offset + items.length) < total if total
220
+
221
+ items.length == page_limit
222
+ end
201
223
  end
202
224
  end
203
225
  end
@@ -11,6 +11,9 @@ module Legion
11
11
  # @param gem_name [String] short name for the library (e.g. 'llm', 'apollo')
12
12
  # @param routes_module [Module] a Sinatra::Extension module to register
13
13
  def self.register_library_routes(gem_name, routes_module)
14
+ existing = router.library_routes[gem_name.to_s]
15
+ return routes_module if existing == routes_module
16
+
14
17
  router.register_library(gem_name, routes_module)
15
18
  register routes_module
16
19
  end
@@ -366,8 +366,11 @@ module Legion
366
366
  stream do |out|
367
367
  full_text = +''
368
368
  pipeline_response = executor.call_stream do |chunk|
369
- full_text << chunk
370
- out << "event: text-delta\ndata: #{Legion::JSON.dump({ delta: chunk })}\n\n"
369
+ text = chunk.respond_to?(:content) ? chunk.content.to_s : chunk.to_s
370
+ next if text.empty?
371
+
372
+ full_text << text
373
+ out << "event: text-delta\ndata: #{Legion::JSON.dump({ delta: text })}\n\n"
371
374
  end
372
375
 
373
376
  if pipeline_response.tools.is_a?(Array) && !pipeline_response.tools.empty?
@@ -13,14 +13,13 @@ module Legion
13
13
  end
14
14
 
15
15
  app.post '/api/tenants' do
16
- params = parsed_body
16
+ body = parse_request_body
17
17
  result = Legion::Tenants.create(
18
- tenant_id: params['tenant_id'],
19
- name: params['name'],
20
- max_workers: params['max_workers'] || 10
18
+ tenant_id: body[:tenant_id],
19
+ name: body[:name],
20
+ max_workers: body[:max_workers] || 10
21
21
  )
22
- status result[:error] ? 409 : 201
23
- json_response(data: result)
22
+ json_response(result, status_code: result[:error] ? 409 : 201)
24
23
  end
25
24
 
26
25
  app.get '/api/tenants/:tenant_id' do
@@ -167,20 +167,7 @@ module Legion
167
167
  end
168
168
 
169
169
  stream do |out|
170
- Thread.new do
171
- loop do
172
- event = queue.pop
173
- data = Legion::JSON.dump({ **event.transform_keys(&:to_s) })
174
- out << "event: #{event[:event]}\ndata: #{data}\n\n"
175
- rescue IOError, Errno::EPIPE
176
- break
177
- end
178
- ensure
179
- Legion::Events.off('*', listener)
180
- end
181
-
182
- out.callback { Legion::Events.off('*', listener) }
183
- out.errback { Legion::Events.off('*', listener) }
170
+ Routes::Events.stream_queue(out: out, queue: queue, listener: listener)
184
171
  end
185
172
  else
186
173
  count = (params[:count] || 25).to_i
data/lib/legion/api.rb CHANGED
@@ -4,6 +4,7 @@ require 'sinatra/base'
4
4
  require 'legion/json'
5
5
  require_relative 'events'
6
6
  require_relative 'readiness'
7
+ require_relative 'api/default_settings'
7
8
 
8
9
  require_relative 'api/middleware/auth'
9
10
  require_relative 'api/middleware/body_limit'
@@ -148,6 +149,25 @@ module Legion
148
149
  def router
149
150
  @router ||= Legion::API::Router.new
150
151
  end
152
+
153
+ def mount_library_routes(gem_name, fallback_module, preferred_constant_path)
154
+ preferred = constant_from_path(preferred_constant_path)
155
+ if preferred.is_a?(Module)
156
+ register_library_routes(gem_name, preferred)
157
+ elsif router.library_names.include?(gem_name)
158
+ register_library_routes(gem_name, router.library_routes.fetch(gem_name))
159
+ else
160
+ register fallback_module
161
+ end
162
+ end
163
+
164
+ private
165
+
166
+ def constant_from_path(path)
167
+ path.to_s.split('::').reject(&:empty?).reduce(Object) { |scope, name| scope.const_get(name) }
168
+ rescue NameError
169
+ nil
170
+ end
151
171
  end
152
172
 
153
173
  # Mount route modules
@@ -174,14 +194,14 @@ module Legion
174
194
  register Routes::Capacity
175
195
  register Routes::Audit
176
196
  register Routes::Metrics
177
- register Routes::Llm unless router.library_names.include?('llm')
197
+ mount_library_routes('llm', Routes::Llm, 'Legion::LLM::Routes')
178
198
  register Routes::ExtensionCatalog
179
199
  register Routes::OrgChart
180
200
  register Routes::Governance
181
201
  register Routes::Acp
182
202
  register Routes::Prompts
183
203
  register Routes::Marketplace
184
- register Routes::Apollo unless router.library_names.include?('apollo')
204
+ mount_library_routes('apollo', Routes::Apollo, 'Legion::Apollo::Routes')
185
205
  register Routes::Costs
186
206
  register Routes::Traces
187
207
  register Routes::Stats
@@ -8,8 +8,14 @@ module Legion
8
8
  module CLI
9
9
  class Chat
10
10
  module ChatLogger
11
- LOG_DIR = File.expand_path('~/.legion')
11
+ LOG_DIR = File.expand_path('~/.legion')
12
12
  LOG_FILE = File.join(LOG_DIR, 'legion-chat.log')
13
+ LEVELS = {
14
+ 'debug' => ::Logger::DEBUG,
15
+ 'info' => ::Logger::INFO,
16
+ 'warn' => ::Logger::WARN,
17
+ 'error' => ::Logger::ERROR
18
+ }.freeze
13
19
 
14
20
  class << self
15
21
  attr_reader :logger
@@ -22,20 +28,21 @@ module Legion
22
28
  @logger
23
29
  end
24
30
 
25
- def debug(msg) = logger&.debug(msg)
26
- def info(msg) = logger&.info(msg)
27
- def warn(msg) = logger&.warn(msg)
28
- def error(msg) = logger&.error(msg)
31
+ def debug(msg) = logger&.debug(msg)
32
+
33
+ def info(msg) = logger&.info(msg)
34
+
35
+ def warn(msg) = logger&.warn(msg)
36
+
37
+ def error(msg) = logger&.error(msg)
29
38
 
30
39
  private
31
40
 
32
- def parse_level(level)
33
- case level.to_s
34
- when 'debug' then ::Logger::DEBUG
35
- when 'warn' then ::Logger::WARN
36
- when 'error' then ::Logger::ERROR
37
- else ::Logger::INFO
38
- end
41
+ def parse_level(level = 'info')
42
+ normalized_level = level.to_s.strip.downcase
43
+ return ::Logger::INFO if normalized_level.empty?
44
+
45
+ LEVELS.fetch(normalized_level, ::Logger::INFO)
39
46
  end
40
47
 
41
48
  def format_entry(severity, datetime, _progname, msg)
@@ -260,8 +260,10 @@ module Legion
260
260
 
261
261
  def check_api(_options)
262
262
  require 'legion/api'
263
- port = (Legion::Settings[:api] || {})[:port] || 4567
264
- bind = (Legion::Settings[:api] || {})[:bind] || '127.0.0.1'
263
+ api_settings = Legion::Settings[:api]
264
+ port = api_settings[:port]
265
+ configured_bind = api_settings[:bind]
266
+ bind = %w[127.0.0.1 localhost ::1].include?(configured_bind) ? configured_bind : '127.0.0.1'
265
267
 
266
268
  Legion::API.set :port, port
267
269
  Legion::API.set :bind, bind
@@ -101,7 +101,7 @@ module Legion
101
101
  end
102
102
 
103
103
  desc 'validate', 'Validate current configuration'
104
- def validate # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
104
+ def validate # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
105
105
  out = formatter
106
106
  Connection.config_dir = options[:config_dir] if options[:config_dir]
107
107
 
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'legion/logging'
4
+
3
5
  module Legion
4
6
  module CLI
5
7
  module ErrorHandler
8
+ extend Legion::Logging::Helper
9
+
6
10
  PATTERNS = [
7
11
  {
8
12
  match: /connection refused.*5672|ECONNREFUSED.*5672|bunny.*not connected/i,
@@ -71,11 +75,13 @@ module Legion
71
75
  def wrap(error)
72
76
  pattern = PATTERNS.find { |p| error.message.match?(p[:match]) }
73
77
  unless pattern
74
- Legion::Logging.error("[CLI] unhandled error: #{error.class} #{error.message}") if logging_available?
78
+ handle_exception(error, level: :error, handled: true, operation: :wrap_cli_error, matched: false) if logging_available?
79
+ log.error("[CLI] unhandled error: #{error.class} - #{error.message}") if logging_available?
75
80
  return error
76
81
  end
77
82
 
78
- Legion::Logging.warn("[CLI] matched error pattern :#{pattern[:code]} — #{error.message}") if logging_available?
83
+ handle_exception(error, level: :warn, handled: true, operation: :wrap_cli_error, code: pattern[:code]) if logging_available?
84
+ log.warn("[CLI] matched error pattern :#{pattern[:code]} - #{error.message}") if logging_available?
79
85
  Error.actionable(
80
86
  code: pattern[:code],
81
87
  message: "#{pattern[:message]}: #{error.message}",
@@ -10,7 +10,7 @@ module Legion
10
10
  ENV['LEGION_LOCAL'] = 'true'
11
11
  end
12
12
 
13
- log_level = options[:log_level] || 'info'
13
+ log_level = options[:log_level]
14
14
 
15
15
  # Load settings early, before any legion-* gem requires can trigger auto-load.
16
16
  # This ensures DNS bootstrap and config file loading happen exactly once.
@@ -26,7 +26,8 @@ module Legion
26
26
  clear_log_file unless options[:daemonize]
27
27
 
28
28
  api = options.fetch(:api, true)
29
- service_opts = { log_level: log_level, api: api }
29
+ service_opts = { api: api }
30
+ service_opts[:log_level] = log_level if log_level
30
31
  service_opts[:http_port] = options[:http_port] if options[:http_port]
31
32
  service_opts[:role] = :lite if options[:lite]
32
33
  Legion.instance_variable_set(:@service, Legion::Service.new(**service_opts))
data/lib/legion/cli.rb CHANGED
@@ -90,7 +90,7 @@ module Legion
90
90
  end
91
91
 
92
92
  def self.start(given_args = ARGV, config = {})
93
- super
93
+ super(normalize_help_args(given_args), config)
94
94
  rescue Legion::CLI::Error => e
95
95
  Legion::Logging.error("CLI::Main.start CLI error: #{e.message}") if defined?(Legion::Logging)
96
96
  formatter = Output::Formatter.new(json: given_args.include?('--json'), color: !given_args.include?('--no-color'))
@@ -106,6 +106,17 @@ module Legion
106
106
  exit(1)
107
107
  end
108
108
 
109
+ def self.normalize_help_args(given_args)
110
+ args = Array(given_args).dup
111
+ return args unless args.length == 2
112
+ return args unless %w[--help -h].include?(args.last)
113
+
114
+ command = args.first
115
+ return args if command.start_with?('-') || command == 'help'
116
+
117
+ ['help', command]
118
+ end
119
+
109
120
  LEGION_GEMS = %w[
110
121
  legion-transport legion-cache legion-crypt legion-data
111
122
  legion-json legion-logging legion-settings
@@ -163,7 +174,7 @@ module Legion
163
174
  option :pidfile, type: :string, aliases: ['-p'], desc: 'PID file path'
164
175
  option :logfile, type: :string, aliases: ['-l'], desc: 'Log file path'
165
176
  option :time_limit, type: :numeric, aliases: ['-t'], desc: 'Run for N seconds then exit'
166
- option :log_level, type: :string, default: 'info', desc: 'Log level (debug, info, warn, error)'
177
+ option :log_level, type: :string, desc: 'Log level (debug, info, warn, error)'
167
178
  option :api, type: :boolean, default: true, desc: 'Start the HTTP API server'
168
179
  option :http_port, type: :numeric, desc: 'HTTP API port (overrides settings)'
169
180
  option :lite, type: :boolean, default: false, desc: 'Start in lite mode (no external services)'
@@ -51,6 +51,24 @@ module Legion
51
51
  Legion::Logging.debug "[Context] session cleared: #{ctx&.session_id}" if defined?(Legion::Logging)
52
52
  Thread.current[:legion_session_context] = nil
53
53
  end
54
+
55
+ def current_task_context
56
+ Thread.current[:legion_context]
57
+ end
58
+
59
+ def with_task_context(message)
60
+ previous = Thread.current[:legion_context]
61
+ Thread.current[:legion_context] = {
62
+ task_id: message[:task_id],
63
+ conversation_id: message[:conversation_id],
64
+ chain_id: message[:chain_id],
65
+ function: message[:function],
66
+ runner_class: message[:runner_class]
67
+ }.compact
68
+ yield
69
+ ensure
70
+ Thread.current[:legion_context] = previous
71
+ end
54
72
  end
55
73
  end
56
74
  end
@@ -16,9 +16,12 @@ module Legion
16
16
  define_dsl_accessor :remote_invocable, default: true
17
17
 
18
18
  def runner
19
- Legion::Runner.run(runner_class: runner_class, function: function, check_subtask: check_subtask?, generate_task: generate_task?)
19
+ with_log_context(function) do
20
+ Legion::Runner.run(runner_class: runner_class, function: function,
21
+ check_subtask: check_subtask?, generate_task: generate_task?)
22
+ end
20
23
  rescue StandardError => e
21
- Legion::Logging.log_exception(e, component_type: :actor)
24
+ handle_exception(e)
22
25
  end
23
26
 
24
27
  def manual
@@ -31,7 +34,7 @@ module Legion
31
34
  klass.send(func, **args)
32
35
  end
33
36
  rescue StandardError => e
34
- Legion::Logging.log_exception(e, component_type: :actor)
37
+ handle_exception(e)
35
38
  end
36
39
 
37
40
  def function
@@ -24,7 +24,8 @@ module Legion
24
24
  log.debug "[Every] tick: #{self.class}" if defined?(log)
25
25
  skip_or_run { use_runner? ? runner : manual }
26
26
  rescue StandardError => e
27
- log.log_exception(e, payload_summary: "[Every] tick failed for #{self.class}", component_type: :actor) if defined?(log)
27
+ log.error "[Every] tick failed for #{self.class}: #{e.class}: #{e.message}" if defined?(log)
28
+ handle_exception(e) if defined?(log)
28
29
  ensure
29
30
  @executing.make_false
30
31
  end
@@ -35,7 +36,7 @@ module Legion
35
36
 
36
37
  @timer.execute
37
38
  rescue StandardError => e
38
- log.log_exception(e, component_type: :actor)
39
+ handle_exception(e)
39
40
  end
40
41
 
41
42
  def run_now?
@@ -52,7 +53,7 @@ module Legion
52
53
 
53
54
  @timer.shutdown
54
55
  rescue StandardError => e
55
- log.log_exception(e, component_type: :actor)
56
+ handle_exception(e)
56
57
  end
57
58
  end
58
59
  end