activematrix 0.0.7 → 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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +96 -28
  3. data/app/models/active_matrix/agent.rb +36 -1
  4. data/app/models/active_matrix/agent_store.rb +29 -0
  5. data/app/models/active_matrix/application_record.rb +8 -0
  6. data/app/models/active_matrix/chat_session.rb +29 -0
  7. data/app/models/active_matrix/knowledge_base.rb +26 -0
  8. data/exe/activematrix +7 -0
  9. data/lib/active_matrix/agent_manager.rb +160 -121
  10. data/lib/active_matrix/agent_registry.rb +25 -21
  11. data/lib/active_matrix/api.rb +8 -2
  12. data/lib/active_matrix/async_query.rb +58 -0
  13. data/lib/active_matrix/bot/base.rb +3 -3
  14. data/lib/active_matrix/bot/builtin_commands.rb +188 -0
  15. data/lib/active_matrix/bot/command_parser.rb +175 -0
  16. data/lib/active_matrix/cli.rb +273 -0
  17. data/lib/active_matrix/client.rb +21 -6
  18. data/lib/active_matrix/client_pool.rb +38 -27
  19. data/lib/active_matrix/daemon/probe_server.rb +118 -0
  20. data/lib/active_matrix/daemon/signal_handler.rb +156 -0
  21. data/lib/active_matrix/daemon/worker.rb +109 -0
  22. data/lib/active_matrix/daemon.rb +236 -0
  23. data/lib/active_matrix/engine.rb +5 -1
  24. data/lib/active_matrix/errors.rb +1 -1
  25. data/lib/active_matrix/event_router.rb +61 -49
  26. data/lib/active_matrix/events.rb +1 -0
  27. data/lib/active_matrix/instrumentation.rb +148 -0
  28. data/lib/active_matrix/memory/agent_memory.rb +7 -21
  29. data/lib/active_matrix/memory/conversation_memory.rb +4 -20
  30. data/lib/active_matrix/memory/global_memory.rb +15 -30
  31. data/lib/active_matrix/message_dispatcher.rb +197 -0
  32. data/lib/active_matrix/metrics.rb +424 -0
  33. data/lib/active_matrix/presence_manager.rb +181 -0
  34. data/lib/active_matrix/telemetry.rb +134 -0
  35. data/lib/active_matrix/version.rb +1 -1
  36. data/lib/active_matrix.rb +12 -2
  37. data/lib/generators/active_matrix/install/install_generator.rb +3 -15
  38. data/lib/generators/active_matrix/install/templates/README +5 -2
  39. metadata +141 -45
  40. data/lib/active_matrix/protocols/cs/message_relationships.rb +0 -318
  41. data/lib/generators/active_matrix/install/templates/create_agent_memories.rb +0 -17
  42. data/lib/generators/active_matrix/install/templates/create_conversation_contexts.rb +0 -21
  43. data/lib/generators/active_matrix/install/templates/create_global_memories.rb +0 -20
  44. data/lib/generators/active_matrix/install/templates/create_matrix_agents.rb +0 -26
@@ -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)
@@ -10,7 +10,7 @@ module ActiveMatrix
10
10
  extend ActiveMatrix::Extensions
11
11
  include ActiveMatrix::Logging
12
12
 
13
- USER_AGENT = "Ruby Matrix SDK v#{ActiveMatrix::VERSION}".freeze
13
+ USER_AGENT = "ActiveMatrix v#{ActiveMatrix::VERSION}".freeze
14
14
  DEFAULT_HEADERS = {
15
15
  'accept' => 'application/json',
16
16
  'user-agent' => USER_AGENT
@@ -287,7 +287,7 @@ module ActiveMatrix
287
287
  end
288
288
 
289
289
  failures = 0
290
- loop do
290
+ loop do # rubocop:disable Metrics/BlockLength
291
291
  raise MatrixConnectionError, "Server still too busy to handle request after #{failures} attempts, try again later" if failures >= 10
292
292
 
293
293
  req_id = ('A'..'Z').to_a.sample(4).join
@@ -343,6 +343,12 @@ module ActiveMatrix
343
343
  end
344
344
  raise MatrixRequestError.new_by_code(data, response.code) if data
345
345
 
346
+ # For 4xx errors without JSON body, construct a synthetic error
347
+ if response.code.to_i >= 400 && response.code.to_i < 500
348
+ synthetic_error = { errcode: 'M_UNKNOWN', error: "HTTP #{response.code} #{response.message}" }
349
+ raise MatrixRequestError.new_by_code(synthetic_error, response.code)
350
+ end
351
+
346
352
  raise MatrixConnectionError.class_by_code(response.code), response
347
353
  end
348
354
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveMatrix
4
+ # Helper methods for async ActiveRecord queries (Rails 8.0+)
5
+ module AsyncQuery
6
+ module_function
7
+
8
+ # Load records asynchronously
9
+ # @param relation [ActiveRecord::Relation] The relation to load
10
+ # @return [Array] The loaded records
11
+ def load_async(relation)
12
+ relation.load_async.to_a
13
+ end
14
+
15
+ # Count records asynchronously
16
+ # @param relation [ActiveRecord::Relation] The relation to count
17
+ # @return [Integer] The count
18
+ def async_count(relation)
19
+ relation.async_count.value
20
+ end
21
+
22
+ # Sum column asynchronously
23
+ # @param relation [ActiveRecord::Relation] The relation
24
+ # @param column [Symbol] The column to sum
25
+ # @return [Numeric] The sum
26
+ def async_sum(relation, column)
27
+ relation.async_sum(column).value
28
+ end
29
+
30
+ # Pluck columns asynchronously
31
+ # @param relation [ActiveRecord::Relation] The relation
32
+ # @param columns [Array<Symbol>] The columns to pluck
33
+ # @return [Array] The plucked values
34
+ def async_pluck(relation, *columns)
35
+ relation.async_pluck(*columns).value
36
+ end
37
+
38
+ # Check existence asynchronously
39
+ # @param relation [ActiveRecord::Relation] The relation
40
+ # @return [Boolean] Whether records exist
41
+ def async_exists?(relation)
42
+ relation.async_count.value.positive?
43
+ end
44
+
45
+ # Execute multiple async queries in parallel and wait for all results
46
+ # @param queries [Hash<Symbol, Proc>] Named queries to execute
47
+ # @return [Hash<Symbol, Object>] Results keyed by query name
48
+ # @example
49
+ # results = AsyncQuery.parallel(
50
+ # agents: -> { MatrixAgent.where(state: :online).load_async },
51
+ # count: -> { MatrixAgent.async_count }
52
+ # )
53
+ def parallel(**queries)
54
+ promises = queries.transform_values(&:call)
55
+ promises.transform_values { |result| result.respond_to?(:value) ? result.value : result }
56
+ end
57
+ end
58
+ end
@@ -377,7 +377,7 @@ module ActiveMatrix::Bot
377
377
  if settings.store_sync_token
378
378
  begin
379
379
  active_bot.client.api.set_account_data(
380
- active_bot.client.mxid, "dev.ananace.ruby-sdk.#{settings.bot_name}",
380
+ active_bot.client.mxid, "com.seuros.active_matrix.#{settings.bot_name}",
381
381
  { sync_token: active_bot.client.sync_token }
382
382
  )
383
383
  rescue StandardError => e
@@ -466,7 +466,7 @@ module ActiveMatrix::Bot
466
466
  bot.client.instance_variable_set(:@next_batch, settings.sync_token)
467
467
  elsif settings.store_sync_token?
468
468
  begin
469
- data = bot.client.api.get_account_data(bot.client.mxid, "dev.ananace.ruby-sdk.#{bot_name}")
469
+ data = bot.client.api.get_account_data(bot.client.mxid, "com.seuros.active_matrix.#{bot_name}")
470
470
  bot.client.sync_token = data[:sync_token]
471
471
  rescue ActiveMatrix::MatrixNotFoundError
472
472
  # Valid
@@ -479,7 +479,7 @@ module ActiveMatrix::Bot
479
479
 
480
480
  bot.client.start_listener_thread
481
481
 
482
- bot.client.instance_variable_get(:@sync_thread).join
482
+ bot.client.sync_thread&.join
483
483
  rescue Interrupt
484
484
  # Happens when killed
485
485
  rescue StandardError => e