activematrix 0.0.5 → 0.0.8

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +96 -28
  3. data/app/jobs/active_matrix/application_job.rb +11 -0
  4. data/app/models/active_matrix/agent/jobs/memory_reaper.rb +87 -0
  5. data/app/models/active_matrix/agent.rb +166 -0
  6. data/app/models/active_matrix/agent_store.rb +80 -0
  7. data/app/models/active_matrix/application_record.rb +15 -0
  8. data/app/models/active_matrix/chat_session.rb +105 -0
  9. data/app/models/active_matrix/knowledge_base.rb +100 -0
  10. data/exe/activematrix +7 -0
  11. data/lib/active_matrix/agent_manager.rb +160 -121
  12. data/lib/active_matrix/agent_registry.rb +25 -21
  13. data/lib/active_matrix/api.rb +8 -2
  14. data/lib/active_matrix/async_query.rb +58 -0
  15. data/lib/active_matrix/bot/base.rb +3 -3
  16. data/lib/active_matrix/bot/builtin_commands.rb +188 -0
  17. data/lib/active_matrix/bot/command_parser.rb +175 -0
  18. data/lib/active_matrix/cli.rb +273 -0
  19. data/lib/active_matrix/client.rb +21 -6
  20. data/lib/active_matrix/client_pool.rb +38 -27
  21. data/lib/active_matrix/daemon/probe_server.rb +118 -0
  22. data/lib/active_matrix/daemon/signal_handler.rb +156 -0
  23. data/lib/active_matrix/daemon/worker.rb +109 -0
  24. data/lib/active_matrix/daemon.rb +236 -0
  25. data/lib/active_matrix/engine.rb +18 -0
  26. data/lib/active_matrix/errors.rb +1 -1
  27. data/lib/active_matrix/event_router.rb +61 -49
  28. data/lib/active_matrix/events.rb +1 -0
  29. data/lib/active_matrix/instrumentation.rb +148 -0
  30. data/lib/active_matrix/memory/agent_memory.rb +7 -21
  31. data/lib/active_matrix/memory/conversation_memory.rb +4 -20
  32. data/lib/active_matrix/memory/global_memory.rb +15 -30
  33. data/lib/active_matrix/message_dispatcher.rb +197 -0
  34. data/lib/active_matrix/metrics.rb +424 -0
  35. data/lib/active_matrix/presence_manager.rb +181 -0
  36. data/lib/active_matrix/railtie.rb +8 -0
  37. data/lib/active_matrix/telemetry.rb +134 -0
  38. data/lib/active_matrix/version.rb +1 -1
  39. data/lib/active_matrix.rb +18 -11
  40. data/lib/generators/active_matrix/install/install_generator.rb +3 -22
  41. data/lib/generators/active_matrix/install/templates/README +5 -2
  42. metadata +191 -31
  43. data/lib/generators/active_matrix/install/templates/agent_memory.rb +0 -47
  44. data/lib/generators/active_matrix/install/templates/conversation_context.rb +0 -72
  45. data/lib/generators/active_matrix/install/templates/create_agent_memories.rb +0 -17
  46. data/lib/generators/active_matrix/install/templates/create_conversation_contexts.rb +0 -21
  47. data/lib/generators/active_matrix/install/templates/create_global_memories.rb +0 -20
  48. data/lib/generators/active_matrix/install/templates/create_matrix_agents.rb +0 -26
  49. data/lib/generators/active_matrix/install/templates/global_memory.rb +0 -70
  50. data/lib/generators/active_matrix/install/templates/matrix_agent.rb +0 -127
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ # <rails-lens:schema:begin>
4
+ # table = "active_matrix_chat_sessions"
5
+ # database_dialect = "PostgreSQL"
6
+ #
7
+ # columns = [
8
+ # { name = "id", type = "integer", pk = true, null = false },
9
+ # { name = "agent_id", type = "integer", null = false },
10
+ # { name = "user_id", type = "string", null = false },
11
+ # { name = "room_id", type = "string", null = false },
12
+ # { name = "context", type = "json" },
13
+ # { name = "message_history", type = "json" },
14
+ # { name = "last_message_at", type = "datetime" },
15
+ # { name = "message_count", type = "integer", null = false, default = "0" },
16
+ # { name = "created_at", type = "datetime", null = false },
17
+ # { name = "updated_at", type = "datetime", null = false }
18
+ # ]
19
+ #
20
+ # indexes = [
21
+ # { name = "index_active_matrix_chat_sessions_on_agent_id", columns = ["agent_id"] },
22
+ # { name = "index_active_matrix_chat_sessions_on_last_message_at", columns = ["last_message_at"] },
23
+ # { name = "index_chat_sessions_on_agent_user_room", columns = ["agent_id", "user_id", "room_id"], unique = true }
24
+ # ]
25
+ #
26
+ # foreign_keys = [
27
+ # { column = "agent_id", references_table = "active_matrix_agents", references_column = "id", name = "fk_rails_53457da357" }
28
+ # ]
29
+ #
30
+ # notes = ["agent:INVERSE_OF", "context:NOT_NULL", "message_history:NOT_NULL", "user_id:LIMIT", "room_id:LIMIT"]
31
+ # <rails-lens:schema:end>
32
+ module ActiveMatrix
33
+ class ChatSession < ApplicationRecord
34
+ self.table_name = 'active_matrix_chat_sessions'
35
+
36
+ belongs_to :agent, class_name: 'ActiveMatrix::Agent'
37
+
38
+ validates :user_id, presence: true
39
+ validates :room_id, presence: true
40
+ validates :user_id, uniqueness: { scope: %i[agent_id room_id] }
41
+
42
+ # Configuration
43
+ MAX_HISTORY_SIZE = 20
44
+
45
+ # Scopes
46
+ scope :recent, -> { order(last_message_at: :desc) }
47
+ scope :active, -> { where('last_message_at > ?', 1.hour.ago) }
48
+ scope :stale, -> { where(last_message_at: ...1.day.ago) }
49
+
50
+ # Add a message to the history
51
+ def add_message(message_data)
52
+ messages = message_history['messages'] || []
53
+
54
+ # Add new message
55
+ messages << {
56
+ 'event_id' => message_data[:event_id],
57
+ 'sender' => message_data[:sender],
58
+ 'content' => message_data[:content],
59
+ 'timestamp' => message_data[:timestamp] || Time.current.to_i
60
+ }
61
+
62
+ # Keep only recent messages
63
+ messages = messages.last(MAX_HISTORY_SIZE)
64
+
65
+ # Update record
66
+ self.message_history = { 'messages' => messages }
67
+ self.last_message_at = Time.current
68
+ self.message_count = messages.size
69
+ save!
70
+
71
+ # Update cache
72
+ write_to_cache
73
+ end
74
+
75
+ # Get recent messages
76
+ def recent_messages(limit = 10)
77
+ messages = message_history['messages'] || []
78
+ messages.last(limit)
79
+ end
80
+
81
+ # Clear old messages but keep context
82
+ def prune_history!
83
+ messages = message_history['messages'] || []
84
+ self.message_history = { 'messages' => messages.last(5) }
85
+ save!
86
+ end
87
+
88
+ # Cache integration
89
+ def cache_key
90
+ "conversation/#{agent_id}/#{user_id}/#{room_id}"
91
+ end
92
+
93
+ def write_to_cache
94
+ Rails.cache.write(cache_key, {
95
+ context: context,
96
+ recent_messages: recent_messages,
97
+ last_message_at: last_message_at
98
+ }, expires_in: 1.hour)
99
+ end
100
+
101
+ def self.cleanup_stale!
102
+ stale.destroy_all
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ # <rails-lens:schema:begin>
4
+ # table = "active_matrix_knowledge_bases"
5
+ # database_dialect = "PostgreSQL"
6
+ #
7
+ # columns = [
8
+ # { name = "id", type = "integer", pk = true, null = false },
9
+ # { name = "key", type = "string", null = false },
10
+ # { name = "value", type = "json" },
11
+ # { name = "category", type = "string" },
12
+ # { name = "expires_at", type = "datetime" },
13
+ # { name = "public_read", type = "boolean", null = false, default = "true" },
14
+ # { name = "public_write", type = "boolean", null = false, default = "false" },
15
+ # { name = "created_at", type = "datetime", null = false },
16
+ # { name = "updated_at", type = "datetime", null = false }
17
+ # ]
18
+ #
19
+ # indexes = [
20
+ # { name = "index_active_matrix_knowledge_bases_on_category", columns = ["category"] },
21
+ # { name = "index_active_matrix_knowledge_bases_on_expires_at", columns = ["expires_at"] },
22
+ # { name = "index_active_matrix_knowledge_bases_on_key", columns = ["key"], unique = true },
23
+ # { name = "index_active_matrix_knowledge_bases_on_public_read", columns = ["public_read"] },
24
+ # { name = "index_active_matrix_knowledge_bases_on_public_write", columns = ["public_write"] }
25
+ # ]
26
+ #
27
+ # notes = ["value:NOT_NULL", "category:NOT_NULL", "key:LIMIT", "category:LIMIT"]
28
+ # <rails-lens:schema:end>
29
+ module ActiveMatrix
30
+ class KnowledgeBase < ApplicationRecord
31
+ self.table_name = 'active_matrix_knowledge_bases'
32
+
33
+ validates :key, presence: true, uniqueness: true
34
+
35
+ scope :active, -> { where('expires_at IS NULL OR expires_at > ?', Time.current) }
36
+ scope :expired, -> { where(expires_at: ..Time.current) }
37
+ scope :by_category, ->(category) { where(category: category) }
38
+ scope :readable, -> { where(public_read: true) }
39
+ scope :writable, -> { where(public_write: true) }
40
+
41
+ def expired?
42
+ expires_at.present? && expires_at <= Time.current
43
+ end
44
+
45
+ def readable_by?(agent)
46
+ public_read || (agent.is_a?(ActiveMatrix::Agent) && agent.admin?)
47
+ end
48
+
49
+ def writable_by?(agent)
50
+ public_write || (agent.is_a?(ActiveMatrix::Agent) && agent.admin?)
51
+ end
52
+
53
+ # Cache integration
54
+ def cache_key
55
+ "global/#{key}"
56
+ end
57
+
58
+ def write_to_cache
59
+ return unless active?
60
+
61
+ ttl = expires_at.present? ? expires_at - Time.current : nil
62
+ Rails.cache.write(cache_key, value, expires_in: ttl)
63
+ end
64
+
65
+ def self.get(key)
66
+ # Try cache first
67
+ cached = Rails.cache.read("global/#{key}")
68
+ return cached if cached.present?
69
+
70
+ # Fallback to database
71
+ memory = find_by(key: key)
72
+ return unless memory&.active?
73
+
74
+ memory.write_to_cache
75
+ memory.value
76
+ end
77
+
78
+ def self.set(key, value, category: nil, expires_in: nil, public_read: true, public_write: false)
79
+ memory = find_or_initialize_by(key: key)
80
+ memory.value = value
81
+ memory.category = category
82
+ memory.expires_at = expires_in.present? ? Time.current + expires_in : nil
83
+ memory.public_read = public_read
84
+ memory.public_write = public_write
85
+ memory.save!
86
+ memory.write_to_cache
87
+ memory
88
+ end
89
+
90
+ def self.cleanup_expired!
91
+ expired.destroy_all
92
+ end
93
+
94
+ private
95
+
96
+ def active?
97
+ !expired?
98
+ end
99
+ end
100
+ end
data/exe/activematrix ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'active_matrix/cli'
6
+
7
+ ActiveMatrix::CLI.start(ARGV)
@@ -1,43 +1,88 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'singleton'
4
+ require 'async'
5
+ require 'async/barrier'
6
+ require 'async/semaphore'
4
7
 
5
8
  module ActiveMatrix
6
- # Manages the lifecycle of Matrix bot agents
9
+ # Manages the lifecycle of Matrix bot agents using async fibers
7
10
  class AgentManager
8
11
  include Singleton
9
12
  include ActiveMatrix::Logging
10
13
 
14
+ # OpenTelemetry semantic conventions for messaging
15
+ OTEL_ATTRS = {
16
+ service: 'activematrix.agent_manager',
17
+ messaging_system: 'matrix'
18
+ }.freeze
19
+
11
20
  attr_reader :registry, :config
12
21
 
13
22
  def initialize
14
23
  @registry = AgentRegistry.instance
15
24
  @config = ActiveMatrix.config
16
- @shutdown = false
17
- @monitor_thread = nil
25
+ @barrier = nil
26
+ @monitor_task = nil
27
+ @running = false
28
+ end
18
29
 
19
- setup_signal_handlers
30
+ # Install signal handlers for graceful shutdown.
31
+ # Call this explicitly if you want the gem to handle SIGINT/SIGTERM.
32
+ # By default, signal handling is left to the host application.
33
+ def install_signal_handlers!
34
+ %w[INT TERM].each do |signal|
35
+ Signal.trap(signal) do
36
+ stop_all
37
+ exit # rubocop:disable Rails/Exit
38
+ end
39
+ end
20
40
  end
21
41
 
22
42
  # Start all agents marked as active in the database
43
+ # This is the main entry point - runs the async reactor
23
44
  def start_all
24
- return if @shutdown
45
+ agents = ActiveMatrix::Agent.where.not(state: :offline)
46
+ start_agents(agents)
47
+ end
25
48
 
26
- logger.info 'Starting all active agents...'
49
+ # Start specific agents (used by daemon workers)
50
+ # @param agents [ActiveRecord::Relation, Array<Agent>] Agents to start
51
+ def start_agents(agents)
52
+ return if @running
27
53
 
28
- agents = defined?(MatrixAgent) ? MatrixAgent.where.not(state: :offline) : []
29
- agents.each_with_index do |agent, index|
30
- sleep(config.agent_startup_delay || 2) if index.positive?
31
- start_agent(agent)
32
- end
54
+ @running = true
55
+ agents_array = agents.respond_to?(:to_a) ? agents.to_a : agents
56
+ logger.info "Starting #{agents_array.size} agents..."
57
+
58
+ Sync do
59
+ @barrier = Async::Barrier.new
60
+
61
+ # Start the event router for routing Matrix events
62
+ EventRouter.instance.start
33
63
 
34
- start_monitor_thread
35
- logger.info "Started #{@registry.count} agents"
64
+ startup_delay = config.agent_startup_delay || 2
65
+
66
+ agents_array.each_with_index do |agent, index|
67
+ sleep(startup_delay) if index.positive?
68
+ start_agent(agent)
69
+ end
70
+
71
+ start_monitor_task
72
+
73
+ logger.info "Started #{@registry.count} agents"
74
+
75
+ # Wait for all agent tasks to complete (blocks until shutdown)
76
+ @barrier.wait
77
+ ensure
78
+ @barrier&.stop
79
+ @running = false
80
+ end
36
81
  end
37
82
 
38
- # Start a specific agent
83
+ # Start a specific agent as an async task
39
84
  def start_agent(agent)
40
- return if @shutdown
85
+ return false unless @running
41
86
 
42
87
  if @registry.running?(agent)
43
88
  logger.warn "Agent #{agent.name} is already running"
@@ -47,51 +92,15 @@ module ActiveMatrix
47
92
  logger.info "Starting agent: #{agent.name}"
48
93
 
49
94
  begin
50
- # Update state
51
95
  agent.connect!
52
96
 
53
- # Create bot instance in a new thread
54
- thread = Thread.new do
55
- Thread.current.name = "agent-#{agent.name}"
56
-
57
- begin
58
- # Create client and bot instance
59
- client = create_client_for_agent(agent)
60
- bot_class = agent.bot_class.constantize
61
- bot_instance = bot_class.new(client)
62
-
63
- # Register the agent
64
- @registry.register(agent, bot_instance)
65
-
66
- # Authenticate if needed
67
- if agent.access_token.present?
68
- client.access_token = agent.access_token
69
- else
70
- client.login(agent.username, agent.password)
71
- agent.update(access_token: client.access_token)
72
- end
73
-
74
- # Restore sync token if available
75
- client.sync_token = agent.last_sync_token if agent.last_sync_token.present?
76
-
77
- # Mark as online
78
- agent.connection_established!
79
-
80
- # Start the sync loop
81
- client.start_listener_thread
82
- client.instance_variable_get(:@sync_thread).join
83
- rescue StandardError => e
84
- logger.error "Error in agent #{agent.name}: #{e.message}"
85
- logger.error e.backtrace.join("\n")
86
- agent.encounter_error!
87
- raise
88
- ensure
89
- @registry.unregister(agent)
90
- agent.disconnect! if agent.may_disconnect?
91
- end
97
+ task = @barrier.async do |subtask|
98
+ subtask.annotate "agent-#{agent.name}"
99
+ run_agent(agent)
92
100
  end
93
101
 
94
- thread.abort_on_exception = true
102
+ # Store task reference for later control
103
+ @registry.register_task(agent, task)
95
104
  true
96
105
  rescue StandardError => e
97
106
  logger.error "Failed to start agent #{agent.name}: #{e.message}"
@@ -109,18 +118,15 @@ module ActiveMatrix
109
118
 
110
119
  begin
111
120
  # Stop the client sync
112
- client = entry[:instance].client
113
- client.stop_listener_thread if client.listening?
121
+ client = entry[:instance]&.client
122
+ client&.stop_listener if client&.listening?
114
123
 
115
124
  # Save sync token
116
- agent.update(last_sync_token: client.sync_token) if client.sync_token.present?
125
+ agent.update(last_sync_token: client.sync_token) if client&.sync_token.present?
117
126
 
118
- # Kill the thread if still alive
119
- thread = entry[:thread]
120
- if thread&.alive?
121
- thread.kill
122
- thread.join(5) # Wait up to 5 seconds
123
- end
127
+ # Stop the async task gracefully
128
+ task = entry[:task]
129
+ task&.stop
124
130
 
125
131
  # Update state
126
132
  agent.disconnect! if agent.may_disconnect?
@@ -135,16 +141,17 @@ module ActiveMatrix
135
141
  # Stop all running agents
136
142
  def stop_all
137
143
  logger.info 'Stopping all agents...'
138
- @shutdown = true
139
144
 
140
- # Stop monitor thread
141
- @monitor_thread&.kill
145
+ # Stop monitor task
146
+ @monitor_task&.stop
142
147
 
143
- # Stop all agents
144
- @registry.all_records.each do |agent|
145
- stop_agent(agent)
146
- end
148
+ # Stop event router
149
+ EventRouter.instance.stop
147
150
 
151
+ # Stop all agent tasks via barrier
152
+ @barrier&.stop
153
+
154
+ @running = false
148
155
  logger.info 'All agents stopped'
149
156
  end
150
157
 
@@ -164,8 +171,8 @@ module ActiveMatrix
164
171
 
165
172
  logger.info "Pausing agent: #{agent.name}"
166
173
 
167
- client = entry[:instance].client
168
- client.stop_listener_thread if client.listening?
174
+ client = entry[:instance]&.client
175
+ client&.stop_listener if client&.listening?
169
176
  agent.pause!
170
177
 
171
178
  true
@@ -181,8 +188,8 @@ module ActiveMatrix
181
188
  logger.info "Resuming agent: #{agent.name}"
182
189
 
183
190
  agent.resume!
184
- client = entry[:instance].client
185
- client.start_listener_thread
191
+ client = entry[:instance]&.client
192
+ client&.start_listener
186
193
  agent.connection_established!
187
194
 
188
195
  true
@@ -193,41 +200,79 @@ module ActiveMatrix
193
200
  {
194
201
  running: @registry.count,
195
202
  agents: @registry.health_status,
196
- monitor_active: @monitor_thread&.alive? || false,
197
- shutdown: @shutdown
203
+ monitor_active: @monitor_task&.alive? || false,
204
+ shutdown: !@running
198
205
  }
199
206
  end
200
207
 
208
+ # Check if currently running
209
+ def running?
210
+ @running
211
+ end
212
+
201
213
  private
202
214
 
203
- def create_client_for_agent(agent)
204
- # Use shared client pool if available
205
- if defined?(ClientPool)
206
- ClientPool.instance.get_client(agent.homeserver)
207
- else
208
- ActiveMatrix::Client.new(agent.homeserver,
209
- client_cache: :some,
210
- sync_filter_limit: 20)
215
+ def run_agent(agent)
216
+ Telemetry.trace('agent.run', attributes: agent_attributes(agent)) do |span|
217
+ # Create client and bot instance
218
+ client = create_client_for_agent(agent)
219
+ bot_class = agent.bot_class.constantize
220
+ bot_instance = bot_class.new(client)
221
+
222
+ # Register the agent
223
+ @registry.register(agent, bot_instance)
224
+
225
+ # Authenticate if needed
226
+ if agent.access_token.present?
227
+ client.access_token = agent.access_token
228
+ else
229
+ Telemetry.trace('agent.login', attributes: agent_attributes(agent)) do
230
+ client.login(agent.username, agent.password)
231
+ agent.update(access_token: client.access_token)
232
+ end
233
+ end
234
+
235
+ # Restore sync token if available
236
+ client.sync_token = agent.last_sync_token if agent.last_sync_token.present?
237
+
238
+ # Mark as online
239
+ agent.connection_established!
240
+ span&.add_event('agent.connected')
241
+
242
+ # Run the sync loop (blocks until stopped)
243
+ client.listen_forever
211
244
  end
245
+ rescue Async::Stop
246
+ logger.info "Agent #{agent.name} stopping gracefully"
247
+ rescue StandardError => e
248
+ Telemetry.record_exception(e, attributes: agent_attributes(agent))
249
+ logger.error "Error in agent #{agent.name}: #{e.message}"
250
+ logger.error e.backtrace.first(10).join("\n")
251
+ agent.encounter_error!
252
+ raise
253
+ ensure
254
+ @registry.unregister(agent)
255
+ agent.disconnect! if agent.may_disconnect?
212
256
  end
213
257
 
214
- def start_monitor_thread
215
- return if @monitor_thread&.alive?
258
+ def create_client_for_agent(agent)
259
+ ClientPool.instance.get_client(agent.homeserver)
260
+ end
216
261
 
217
- @monitor_thread = Thread.new do
218
- Thread.current.name = 'agent-monitor'
262
+ def start_monitor_task
263
+ return if @monitor_task&.alive?
219
264
 
220
- loop do
221
- break if @shutdown
265
+ health_interval = config.agent_health_check_interval || 30
222
266
 
223
- begin
224
- check_agent_health
225
- cleanup_stale_data
226
- rescue StandardError => e
227
- logger.error "Monitor thread error: #{e.message}"
228
- end
267
+ @monitor_task = Async(transient: true) do |task|
268
+ task.annotate 'agent-monitor'
229
269
 
230
- sleep(config.agent_health_check_interval || 30)
270
+ loop do
271
+ sleep(health_interval)
272
+ check_agent_health
273
+ cleanup_stale_data
274
+ rescue StandardError => e
275
+ logger.error "Monitor task error: #{e.message}"
231
276
  end
232
277
  end
233
278
  end
@@ -235,41 +280,35 @@ module ActiveMatrix
235
280
  def check_agent_health
236
281
  @registry.find_each do |entry|
237
282
  agent = entry[:record]
238
- thread = entry[:thread]
283
+ task = entry[:task]
239
284
 
240
- # Check if thread is alive
241
- unless thread&.alive?
242
- logger.warn "Agent #{agent.name} thread died, restarting..."
285
+ # Check if task is alive
286
+ unless task&.alive?
287
+ logger.warn "Agent #{agent.name} task died, restarting..."
243
288
  @registry.unregister(agent)
244
289
  agent.encounter_error!
245
- start_agent(agent) unless @shutdown
290
+ start_agent(agent) if @running
246
291
  next
247
292
  end
248
293
 
249
294
  # Check last activity
250
- if agent.last_active_at && agent.last_active_at < 5.minutes.ago
251
- logger.warn "Agent #{agent.name} seems inactive"
252
- # Could implement additional health checks here
253
- end
295
+ logger.warn "Agent #{agent.name} seems inactive" if agent.last_active_at && agent.last_active_at < 5.minutes.ago
254
296
  end
255
297
  end
256
298
 
257
299
  def cleanup_stale_data
258
- # Clean up old conversation contexts
259
- ConversationContext.cleanup_stale! if defined?(ConversationContext)
260
-
261
- # Clean up expired memories
262
- AgentMemory.cleanup_expired! if defined?(AgentMemory)
263
- GlobalMemory.cleanup_expired! if defined?(GlobalMemory)
300
+ ActiveMatrix::ChatSession.cleanup_stale!
301
+ ActiveMatrix::AgentStore.cleanup_expired!
302
+ ActiveMatrix::KnowledgeBase.cleanup_expired!
264
303
  end
265
304
 
266
- def setup_signal_handlers
267
- %w[INT TERM].each do |signal|
268
- Signal.trap(signal) do
269
- Thread.new { stop_all }.join
270
- exit # rubocop:disable Rails/Exit
271
- end
272
- end
305
+ def agent_attributes(agent)
306
+ OTEL_ATTRS.merge(
307
+ 'agent.id' => agent.id,
308
+ 'agent.name' => agent.name,
309
+ 'agent.homeserver' => agent.homeserver,
310
+ 'agent.bot_class' => agent.bot_class
311
+ )
273
312
  end
274
313
  end
275
314
  end
@@ -11,32 +11,33 @@ module ActiveMatrix
11
11
 
12
12
  def initialize
13
13
  @agents = Concurrent::Hash.new
14
- @mutex = Mutex.new
15
14
  end
16
15
 
17
16
  # Register a running agent
18
17
  def register(agent_record, bot_instance)
19
- @mutex.synchronize do
20
- raise AgentAlreadyRunningError, "Agent #{agent_record.name} is already running" if @agents.key?(agent_record.id)
21
-
22
- @agents[agent_record.id] = {
23
- record: agent_record,
24
- instance: bot_instance,
25
- thread: Thread.current,
26
- started_at: Time.current
27
- }
18
+ raise AgentAlreadyRunningError, "Agent #{agent_record.name} is already running" if @agents.key?(agent_record.id)
28
19
 
29
- logger.info "Registered agent: #{agent_record.name} (#{agent_record.id})"
30
- end
20
+ @agents[agent_record.id] = {
21
+ record: agent_record,
22
+ instance: bot_instance,
23
+ task: nil,
24
+ started_at: Time.current
25
+ }
26
+
27
+ logger.info "Registered agent: #{agent_record.name} (#{agent_record.id})"
28
+ end
29
+
30
+ # Register an async task for an agent
31
+ def register_task(agent_record, task)
32
+ entry = @agents[agent_record.id]
33
+ entry[:task] = task if entry
31
34
  end
32
35
 
33
36
  # Unregister an agent
34
37
  def unregister(agent_record)
35
- @mutex.synchronize do
36
- entry = @agents.delete(agent_record.id)
37
- logger.info "Unregistered agent: #{agent_record.name} (#{agent_record.id})" if entry
38
- entry
39
- end
38
+ entry = @agents.delete(agent_record.id)
39
+ logger.info "Unregistered agent: #{agent_record.name} (#{agent_record.id})" if entry
40
+ entry
40
41
  end
41
42
 
42
43
  # Get a running agent by ID
@@ -123,9 +124,7 @@ module ActiveMatrix
123
124
 
124
125
  # Clear all agents (used for testing)
125
126
  def clear!
126
- @mutex.synchronize do
127
- @agents.clear
128
- end
127
+ @agents.clear
129
128
  end
130
129
 
131
130
  # Get health status of all agents
@@ -135,13 +134,18 @@ module ActiveMatrix
135
134
  id: id,
136
135
  name: entry[:record].name,
137
136
  state: entry[:record].state,
138
- thread_alive: entry[:thread]&.alive?,
137
+ task_alive: entry[:task]&.alive?,
139
138
  uptime: Time.current - entry[:started_at],
140
139
  last_active: entry[:record].last_active_at
141
140
  }
142
141
  end
143
142
  end
144
143
 
144
+ # Iterate through all agents
145
+ def find_each(&)
146
+ @agents.values.each(&)
147
+ end
148
+
145
149
  private
146
150
 
147
151
  def by_name_pattern(pattern)