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.
- checksums.yaml +4 -4
- data/README.md +96 -28
- data/app/models/active_matrix/agent.rb +36 -1
- data/app/models/active_matrix/agent_store.rb +29 -0
- data/app/models/active_matrix/application_record.rb +8 -0
- data/app/models/active_matrix/chat_session.rb +29 -0
- data/app/models/active_matrix/knowledge_base.rb +26 -0
- data/exe/activematrix +7 -0
- data/lib/active_matrix/agent_manager.rb +160 -121
- data/lib/active_matrix/agent_registry.rb +25 -21
- data/lib/active_matrix/api.rb +8 -2
- data/lib/active_matrix/async_query.rb +58 -0
- data/lib/active_matrix/bot/base.rb +3 -3
- data/lib/active_matrix/bot/builtin_commands.rb +188 -0
- data/lib/active_matrix/bot/command_parser.rb +175 -0
- data/lib/active_matrix/cli.rb +273 -0
- data/lib/active_matrix/client.rb +21 -6
- data/lib/active_matrix/client_pool.rb +38 -27
- data/lib/active_matrix/daemon/probe_server.rb +118 -0
- data/lib/active_matrix/daemon/signal_handler.rb +156 -0
- data/lib/active_matrix/daemon/worker.rb +109 -0
- data/lib/active_matrix/daemon.rb +236 -0
- data/lib/active_matrix/engine.rb +5 -1
- data/lib/active_matrix/errors.rb +1 -1
- data/lib/active_matrix/event_router.rb +61 -49
- data/lib/active_matrix/events.rb +1 -0
- data/lib/active_matrix/instrumentation.rb +148 -0
- data/lib/active_matrix/memory/agent_memory.rb +7 -21
- data/lib/active_matrix/memory/conversation_memory.rb +4 -20
- data/lib/active_matrix/memory/global_memory.rb +15 -30
- data/lib/active_matrix/message_dispatcher.rb +197 -0
- data/lib/active_matrix/metrics.rb +424 -0
- data/lib/active_matrix/presence_manager.rb +181 -0
- data/lib/active_matrix/telemetry.rb +134 -0
- data/lib/active_matrix/version.rb +1 -1
- data/lib/active_matrix.rb +12 -2
- data/lib/generators/active_matrix/install/install_generator.rb +3 -15
- data/lib/generators/active_matrix/install/templates/README +5 -2
- metadata +141 -45
- data/lib/active_matrix/protocols/cs/message_relationships.rb +0 -318
- data/lib/generators/active_matrix/install/templates/create_agent_memories.rb +0 -17
- data/lib/generators/active_matrix/install/templates/create_conversation_contexts.rb +0 -21
- data/lib/generators/active_matrix/install/templates/create_global_memories.rb +0 -20
- 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
|
-
@
|
|
17
|
-
@
|
|
25
|
+
@barrier = nil
|
|
26
|
+
@monitor_task = nil
|
|
27
|
+
@running = false
|
|
28
|
+
end
|
|
18
29
|
|
|
19
|
-
|
|
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
|
-
|
|
45
|
+
agents = ActiveMatrix::Agent.where.not(state: :offline)
|
|
46
|
+
start_agents(agents)
|
|
47
|
+
end
|
|
25
48
|
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
agents.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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]
|
|
113
|
-
client
|
|
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
|
|
125
|
+
agent.update(last_sync_token: client.sync_token) if client&.sync_token.present?
|
|
117
126
|
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
141
|
-
@
|
|
145
|
+
# Stop monitor task
|
|
146
|
+
@monitor_task&.stop
|
|
142
147
|
|
|
143
|
-
# Stop
|
|
144
|
-
|
|
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]
|
|
168
|
-
client
|
|
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]
|
|
185
|
-
client
|
|
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: @
|
|
197
|
-
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
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
215
|
-
|
|
258
|
+
def create_client_for_agent(agent)
|
|
259
|
+
ClientPool.instance.get_client(agent.homeserver)
|
|
260
|
+
end
|
|
216
261
|
|
|
217
|
-
|
|
218
|
-
|
|
262
|
+
def start_monitor_task
|
|
263
|
+
return if @monitor_task&.alive?
|
|
219
264
|
|
|
220
|
-
|
|
221
|
-
break if @shutdown
|
|
265
|
+
health_interval = config.agent_health_check_interval || 30
|
|
222
266
|
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
+
task = entry[:task]
|
|
239
284
|
|
|
240
|
-
# Check if
|
|
241
|
-
unless
|
|
242
|
-
logger.warn "Agent #{agent.name}
|
|
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)
|
|
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
|
-
|
|
259
|
-
|
|
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
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
@
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
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)
|
data/lib/active_matrix/api.rb
CHANGED
|
@@ -10,7 +10,7 @@ module ActiveMatrix
|
|
|
10
10
|
extend ActiveMatrix::Extensions
|
|
11
11
|
include ActiveMatrix::Logging
|
|
12
12
|
|
|
13
|
-
USER_AGENT = "
|
|
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, "
|
|
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, "
|
|
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.
|
|
482
|
+
bot.client.sync_thread&.join
|
|
483
483
|
rescue Interrupt
|
|
484
484
|
# Happens when killed
|
|
485
485
|
rescue StandardError => e
|