legionio 1.6.41 → 1.6.43

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: 7edcbf61a2a48b3302257e76bf2e39c5d7e8ebfe2f871518a33369e960121620
4
- data.tar.gz: f79f0aa5cefe9898d587660e0c86e57ed83d62a46125223aba1e53845f8646f5
3
+ metadata.gz: 17578bcb7fd7292eb90672503f016fbc426e771bec3ca6a4d822aa20019b8979
4
+ data.tar.gz: f31d286761b71e11338608c17a24ae051b9d0a3aab6aec61ae0ecb655c8b01c8
5
5
  SHA512:
6
- metadata.gz: 5086d7e1723b3a65236314f333a87dbbc7391dc86d72a4f530c4788013a8222e2c9e4528fd023ab85313ae9800fa1670a99cdfee31af5eee1ab349642eee2e84
7
- data.tar.gz: 78fccfab7bb20fe009ffc106a656353c82bd2760a580e54b51888b5a172a4807233bc79e259a5169e3d6ed747557cac68cbb14354b7c884bb82f5d1fc4d49c23
6
+ metadata.gz: 811ec04a329c5d791fbcbaecdcec3880bbedccc085016f573634e457998b3e625dfe4bcff3d9ebd37a08251ce671aea15765626b559b2990e28d1dbc938ccecf
7
+ data.tar.gz: 0fba42942c96a2add0b454edd4f5be563f93dd182ac7f8b10f7203062a2ab85aa6a07a9876587f04a16fcd16d22a6f4a4b0aa62a101bd29efc849693d56283c3
data/.rubocop.yml CHANGED
@@ -59,6 +59,7 @@ Metrics/BlockLength:
59
59
  - 'lib/legion/cli/setup_command.rb'
60
60
  - 'lib/legion/cli/trace_command.rb'
61
61
  - 'lib/legion/cli/features_command.rb'
62
+ - 'lib/legion/cli/absorb_command.rb'
62
63
 
63
64
  Metrics/AbcSize:
64
65
  Max: 60
data/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.6.43] - 2026-03-31
6
+
7
+ ### Added
8
+ - `POST /api/absorbers/dispatch` API endpoint for async absorber dispatch — CLI no longer loads extension classes directly
9
+ - Absorb dispatch runs in a background thread, returning job ID immediately
10
+
11
+ ### Changed
12
+ - `legionio absorb url` now routes through the local API instead of loading extension classes in-process (fixes `NameError` when extensions not loaded in CLI context)
13
+ - CLI absorb output updated to show async dispatch status with job ID
14
+
15
+ ## [1.6.42] - 2026-03-31
16
+
17
+ ### Fixed
18
+ - `Every` and `Poll` actors now guard against overlapping executions using `Concurrent::AtomicBoolean` — if the previous tick is still running when the next interval fires, the new tick is skipped with a debug log instead of stacking up concurrent executions
19
+
5
20
  ## [1.6.41] - 2026-03-30
6
21
 
7
22
  ### Fixed
@@ -19,6 +19,26 @@ module Legion
19
19
  json_response(items)
20
20
  end
21
21
 
22
+ app.post '/api/absorbers/dispatch' do
23
+ body = parse_request_body
24
+ input = body[:url] || body[:input]
25
+ halt 400, json_error('missing_param', 'url parameter is required') unless input
26
+
27
+ require 'legion/extensions/actors/absorber_dispatch'
28
+ context = body[:context] || {}
29
+ job_id = SecureRandom.hex(8)
30
+
31
+ Thread.new do
32
+ Legion::Extensions::Actors::AbsorberDispatch.dispatch(
33
+ input: input, job_id: job_id, context: context
34
+ )
35
+ rescue StandardError => e
36
+ Legion::Logging.error("Async absorb #{job_id} failed: #{e.message}") if defined?(Legion::Logging)
37
+ end
38
+
39
+ json_response({ success: true, job_id: job_id, absorber: PatternMatcher.resolve(input)&.name, status: :accepted })
40
+ end
41
+
22
42
  app.get '/api/absorbers/resolve' do
23
43
  input = params[:url] || params[:input]
24
44
  halt 400, json_error('missing_param', 'url parameter is required') unless input
@@ -17,22 +17,16 @@ module Legion
17
17
  desc 'url URL', 'Absorb content from a URL'
18
18
  option :scope, type: :string, default: 'global', desc: 'Knowledge scope (global/local/all)'
19
19
  def url(input_url)
20
- Connection.ensure_settings
21
- require 'legion/extensions/absorbers'
22
- require 'legion/extensions/absorbers/pattern_matcher'
23
- require 'legion/extensions/actors/absorber_dispatch'
24
-
25
20
  out = formatter
26
- result = Legion::Extensions::Actors::AbsorberDispatch.dispatch(
27
- input: input_url,
28
- context: { scope: options[:scope]&.to_sym }
29
- )
21
+ result = api_post('/api/absorbers/dispatch', url: input_url, context: { scope: options[:scope] })
30
22
 
31
23
  if options[:json]
32
24
  out.json(result)
33
25
  elsif result[:success]
34
- out.success("Absorbed: #{input_url}")
35
- out.detail(absorber: result[:absorber], job_id: result[:job_id])
26
+ out.success("Dispatched: #{input_url}")
27
+ puts " absorber: #{result[:absorber]}"
28
+ puts " job_id: #{result[:job_id]}"
29
+ puts ' Processing in background. Check daemon logs for progress.'
36
30
  else
37
31
  out.warn("Failed: #{result[:error]}")
38
32
  end
@@ -84,6 +78,29 @@ module Legion
84
78
  4567
85
79
  end
86
80
 
81
+ def api_post(path, **payload)
82
+ uri = URI("http://127.0.0.1:#{api_port}#{path}")
83
+ http = Net::HTTP.new(uri.host, uri.port)
84
+ http.read_timeout = 300
85
+ request = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json')
86
+ request.body = ::JSON.generate(payload)
87
+ response = http.request(request)
88
+ unless response.is_a?(Net::HTTPSuccess)
89
+ formatter.error("API returned #{response.code} for #{path}")
90
+ raise SystemExit, 1
91
+ end
92
+ body = ::JSON.parse(response.body, symbolize_names: true)
93
+ body[:data]
94
+ rescue Errno::ECONNREFUSED
95
+ formatter.error('Daemon not running. Start with: legionio start')
96
+ raise SystemExit, 1
97
+ rescue SystemExit
98
+ raise
99
+ rescue StandardError => e
100
+ formatter.error("API request failed: #{e.message}")
101
+ raise SystemExit, 1
102
+ end
103
+
87
104
  def api_get(path)
88
105
  uri = URI("http://127.0.0.1:#{api_port}#{path}")
89
106
  response = Net::HTTP.get_response(uri)
@@ -17,12 +17,19 @@ module Legion
17
17
  define_dsl_accessor :run_now, default: false
18
18
 
19
19
  def initialize(**_opts)
20
+ @executing = Concurrent::AtomicBoolean.new(false)
20
21
  @timer = Concurrent::TimerTask.new(execution_interval: time, run_now: run_now?) do
21
- log.debug "[Every] tick: #{self.class}" if defined?(log)
22
- begin
23
- skip_or_run { use_runner? ? runner : manual }
24
- rescue StandardError => e
25
- log.log_exception(e, payload_summary: "[Every] tick failed for #{self.class}", component_type: :actor) if defined?(log)
22
+ if @executing.make_true
23
+ begin
24
+ log.debug "[Every] tick: #{self.class}" if defined?(log)
25
+ skip_or_run { use_runner? ? runner : manual }
26
+ rescue StandardError => e
27
+ log.log_exception(e, payload_summary: "[Every] tick failed for #{self.class}", component_type: :actor) if defined?(log)
28
+ ensure
29
+ @executing.make_false
30
+ end
31
+ elsif defined?(log)
32
+ log.debug "[Every] skipped (previous still running): #{self.class}"
26
33
  end
27
34
  end
28
35
 
@@ -21,10 +21,19 @@ module Legion
21
21
  def initialize
22
22
  log.debug "Starting timer for #{self.class} with #{{ execution_interval: time, run_now: run_now?,
23
23
  check_subtask: check_subtask? }}"
24
+ @executing = Concurrent::AtomicBoolean.new(false)
24
25
  @timer = Concurrent::TimerTask.new(execution_interval: time, run_now: run_now?) do
25
- skip_or_run { poll_cycle }
26
- rescue StandardError => e
27
- Legion::Logging.log_exception(e, level: :fatal, component_type: :actor)
26
+ if @executing.make_true
27
+ begin
28
+ skip_or_run { poll_cycle }
29
+ rescue StandardError => e
30
+ Legion::Logging.log_exception(e, level: :fatal, component_type: :actor)
31
+ ensure
32
+ @executing.make_false
33
+ end
34
+ else
35
+ Legion::Logging.debug "[Poll] skipped (previous still running): #{self.class}"
36
+ end
28
37
  end
29
38
  @timer.execute
30
39
  rescue StandardError => e
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.6.41'
4
+ VERSION = '1.6.43'
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.6.41
4
+ version: 1.6.43
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity