lex-detect 0.1.3 → 0.1.4

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: 119e238ddb0711af3bb4d23c5c65bb85c910420aa4ce79c1fe28c81950a38d0c
4
- data.tar.gz: da4f50f6e3b1653c18d70617ab81630f3f9db6aa01cf6271205882f34df312f6
3
+ metadata.gz: 4be5aba6e8a22792e9f9fcbc2db27f18fe2d51f176044e7a18bdcc7db534becf
4
+ data.tar.gz: de631309f69e478a59d46d7ea9d8f0ff90f4ceff01e198424813e5bece7c00a1
5
5
  SHA512:
6
- metadata.gz: 1f38552069181a4198433a821b64744f6bdf4cd42be0285c22eccfb4ef219f09f5d5d8a5825ac53c95ded7adb45278d5ca9ef20018c96f2dcac1ad61389da296
7
- data.tar.gz: ecb32f15376b6079a1810c161a726f447e1aa6b07a3a187d894fe6393b31d608cc4c0a09227d52aefe88110e6f51ff3f3d9ba1e9e61772463587557600142896
6
+ metadata.gz: 18ef38241cf694a8244900735878fe5e5f49a70e3cedf50db94eb2e902aec8c84ecd3074e79a98320accea445e10b9145eca6590fc5fda4b2efce07e5338f737
7
+ data.tar.gz: af5e47f0566ca56b6b1eee75afd6620a5ec6201363e2c1d68ca89d47681221a0634979a52d9e941764204b3cdeb6745a4d9e33305537f548044c148425b137b0
data/CHANGELOG.md CHANGED
@@ -1,12 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.4] - 2026-03-20
4
+
5
+ ### Changed
6
+ - Version bump for deployment (0.1.3 was released before task observer and cancel task changes landed)
7
+
3
8
  ## [0.1.3] - 2026-03-20
4
9
 
5
10
  ### Added
6
- - `Runners::TaskObserver` for monitoring task failure patterns
11
+ - `Runners::TaskObserver` for monitoring task failure patterns; supports DB-based `observe(since:)` and array-based `observe(tasks:)` calling styles
12
+ - `check_timeout_risk` detects tasks running beyond 2x expected duration and emits `timeout_risk` alert
13
+ - `check_repeated_failure` detects >=3 failures in 10 minutes per runner class
14
+ - `publish_alerts` forwards structured alerts to AMQP when `Legion::Transport` is available
15
+ - `record_observations` persists per-task observation records to `Legion::Data::Local` observer_events table
7
16
  - `check_and_publish_failure_patterns` publishes failure pattern events to AMQP for self-healing pipeline integration
8
17
  - `extract_gem_name` helper derives gem name from runner class string
9
18
  - `build_failure_pattern` creates structured failure pattern hash
19
+ - `Runners::CancelTask` sets `cancelled_at` timestamp on tasks, guarded by `Legion::Data` availability
20
+ - `Actors::ObserverTick` runs `TaskObserver#observe` every 60 seconds, passing `since:` for incremental DB scans
21
+ - Local migration `20260320000001_create_observer_events` creates observer_events table for cognitive observation history
10
22
 
11
23
  ## [0.1.2] - 2026-03-19
12
24
 
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Detect
6
+ module Actors
7
+ class ObserverTick < Legion::Extensions::Actors::Every
8
+ def time = 60
9
+ def run_now? = false
10
+ def use_runner? = false
11
+ def check_subtask? = false
12
+ def generate_task? = false
13
+
14
+ def action(**_opts)
15
+ observer = Object.new.extend(Runners::TaskObserver)
16
+ observer.observe(since: @last_tick || (Time.now - 60))
17
+ ensure
18
+ @last_tick = Time.now
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ up do
5
+ create_table?(:observer_events) do
6
+ primary_key :id
7
+ String :task_id, size: 64
8
+ String :runner, size: 255
9
+ String :rule, size: 64, null: true
10
+ String :severity, size: 16, null: true
11
+ Float :duration, null: true
12
+ Float :token_cost, null: true
13
+ DateTime :observed_at, null: false, default: Sequel::CURRENT_TIMESTAMP
14
+ index :task_id
15
+ index :rule
16
+ index :observed_at
17
+ end
18
+ end
19
+
20
+ down do
21
+ drop_table?(:observer_events)
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Detect
6
+ module Runners
7
+ module CancelTask
8
+ def cancel_task(task_id:, **)
9
+ return { success: false, reason: :data_unavailable } unless defined?(Legion::Data)
10
+
11
+ task = Legion::Data::Model::Task[task_id]
12
+ return { success: false, reason: :not_found } unless task
13
+ return { success: false, reason: :already_cancelled } if task.respond_to?(:cancelled?) && task.cancelled?
14
+
15
+ task.update(cancelled_at: Time.now.utc)
16
+ { success: true, task_id: task_id, cancelled_at: task.cancelled_at }
17
+ rescue StandardError => e
18
+ { success: false, reason: :error, message: e.message }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -7,7 +7,9 @@ module Legion
7
7
  module TaskObserver
8
8
  extend self
9
9
 
10
- def observe(tasks:, **)
10
+ def observe(tasks: nil, since: nil, **)
11
+ return observe_db(since: since) if tasks.nil?
12
+
11
13
  return { success: true, alerts: [] } unless tasks.is_a?(Array)
12
14
 
13
15
  alerts = generate_alerts(tasks)
@@ -19,6 +21,103 @@ module Legion
19
21
 
20
22
  private
21
23
 
24
+ def observe_db(since: nil)
25
+ return { alerts: [], observed: 0 } unless defined?(Legion::Data)
26
+
27
+ since ||= Time.now - 60
28
+ db_tasks = Legion::Data.connection[:tasks]
29
+ .where { started_at > since }
30
+ .all
31
+
32
+ alerts = db_tasks.filter_map { |task| evaluate_rules(task) }
33
+
34
+ publish_alerts(alerts) if alerts.any?
35
+ record_observations(db_tasks, alerts)
36
+
37
+ { alerts: alerts, observed: db_tasks.size }
38
+ rescue StandardError => e
39
+ { alerts: [], observed: 0, error: e.message }
40
+ end
41
+
42
+ def evaluate_rules(task)
43
+ check_timeout_risk(task) ||
44
+ check_repeated_failure(task) ||
45
+ check_cost_spike(task)
46
+ end
47
+
48
+ def check_timeout_risk(task, expected_duration: 120)
49
+ return nil unless task[:status] == 'running' && task[:started_at]
50
+
51
+ elapsed = Time.now - task[:started_at]
52
+ return nil unless elapsed > (expected_duration * 2)
53
+
54
+ {
55
+ rule: 'timeout_risk',
56
+ runner: task[:runner_class],
57
+ task_id: task[:id],
58
+ severity: 'warn',
59
+ detail: "Running for #{elapsed.round}s (expected #{expected_duration}s)",
60
+ observed_at: Time.now.utc
61
+ }
62
+ end
63
+
64
+ def check_repeated_failure(task)
65
+ return nil unless defined?(Legion::Data) && task[:runner_class]
66
+
67
+ count = Legion::Data.connection[:tasks]
68
+ .where(runner_class: task[:runner_class], status: 'failed')
69
+ .where { started_at > Time.now - 600 }
70
+ .count
71
+ return nil unless count >= 3
72
+
73
+ {
74
+ rule: 'repeated_failure',
75
+ runner: task[:runner_class],
76
+ task_id: task[:id],
77
+ severity: 'critical',
78
+ detail: "#{count} failures in last 10 minutes",
79
+ observed_at: Time.now.utc
80
+ }
81
+ rescue StandardError
82
+ nil
83
+ end
84
+
85
+ def check_cost_spike(_task)
86
+ nil
87
+ end
88
+
89
+ def publish_alerts(alerts)
90
+ return unless defined?(Legion::Transport)
91
+
92
+ alerts.each do |alert|
93
+ Legion::Transport::Messages::Dynamic.new(
94
+ function: 'observer_alert',
95
+ payload: alert
96
+ ).publish
97
+ end
98
+ rescue StandardError
99
+ nil
100
+ end
101
+
102
+ def record_observations(db_tasks, alerts)
103
+ return unless defined?(Legion::Data::Local)
104
+
105
+ db_tasks.each do |task|
106
+ alert = alerts.find { |a| a[:task_id] == task[:id] }
107
+ Legion::Data::Local.connection[:observer_events].insert(
108
+ task_id: task[:id],
109
+ runner: task[:runner_class],
110
+ rule: alert&.dig(:rule),
111
+ severity: alert&.dig(:severity),
112
+ duration: task[:started_at] ? (Time.now - task[:started_at]).round(2) : nil,
113
+ token_cost: nil,
114
+ observed_at: Time.now.utc
115
+ )
116
+ end
117
+ rescue StandardError
118
+ nil
119
+ end
120
+
22
121
  def generate_alerts(tasks)
23
122
  failed = tasks.select { |t| t[:status] == 'failed' }
24
123
  return [] if failed.empty?
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Detect
6
- VERSION = '0.1.3'
6
+ VERSION = '0.1.4'
7
7
  end
8
8
  end
9
9
  end
@@ -5,6 +5,7 @@ require 'legion/extensions/detect/catalog'
5
5
  require 'legion/extensions/detect/scanner'
6
6
  require 'legion/extensions/detect/installer'
7
7
  require_relative 'detect/runners/task_observer'
8
+ require_relative 'detect/runners/cancel_task'
8
9
 
9
10
  module Legion
10
11
  module Extensions
@@ -44,6 +45,7 @@ module Legion
44
45
 
45
46
  require_relative 'detect/actors/full_scan' if defined?(Legion::Extensions::Actors::Once)
46
47
  require_relative 'detect/actors/delta_scan' if defined?(Legion::Extensions::Actors::Every)
48
+ require_relative 'detect/actors/observer_tick' if defined?(Legion::Extensions::Actors::Every)
47
49
 
48
50
  if defined?(Legion::Data::Local)
49
51
  Legion::Data::Local.register_migrations(
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-detect
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -29,9 +29,12 @@ files:
29
29
  - lib/legion/extensions/detect.rb
30
30
  - lib/legion/extensions/detect/actors/delta_scan.rb
31
31
  - lib/legion/extensions/detect/actors/full_scan.rb
32
+ - lib/legion/extensions/detect/actors/observer_tick.rb
32
33
  - lib/legion/extensions/detect/catalog.rb
33
34
  - lib/legion/extensions/detect/installer.rb
34
35
  - lib/legion/extensions/detect/local_migrations/20260319000001_create_detect_results.rb
36
+ - lib/legion/extensions/detect/local_migrations/20260320000001_create_observer_events.rb
37
+ - lib/legion/extensions/detect/runners/cancel_task.rb
35
38
  - lib/legion/extensions/detect/runners/task_observer.rb
36
39
  - lib/legion/extensions/detect/scanner.rb
37
40
  - lib/legion/extensions/detect/version.rb