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 +4 -4
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +15 -0
- data/lib/legion/api/absorbers.rb +20 -0
- data/lib/legion/cli/absorb_command.rb +28 -11
- data/lib/legion/extensions/actors/every.rb +12 -5
- data/lib/legion/extensions/actors/poll.rb +12 -3
- 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: 17578bcb7fd7292eb90672503f016fbc426e771bec3ca6a4d822aa20019b8979
|
|
4
|
+
data.tar.gz: f31d286761b71e11338608c17a24ae051b9d0a3aab6aec61ae0ecb655c8b01c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 811ec04a329c5d791fbcbaecdcec3880bbedccc085016f573634e457998b3e625dfe4bcff3d9ebd37a08251ce671aea15765626b559b2990e28d1dbc938ccecf
|
|
7
|
+
data.tar.gz: 0fba42942c96a2add0b454edd4f5be563f93dd182ac7f8b10f7203062a2ab85aa6a07a9876587f04a16fcd16d22a6f4a4b0aa62a101bd29efc849693d56283c3
|
data/.rubocop.yml
CHANGED
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
|
data/lib/legion/api/absorbers.rb
CHANGED
|
@@ -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 =
|
|
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("
|
|
35
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
data/lib/legion/version.rb
CHANGED