collavre_openclaw 0.2.1 → 0.2.2

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: ffa08c6ff97565abd9d9f0c468b5abce5aaa03df463fd529d9fd5f3f08a7aa6d
4
- data.tar.gz: 4bb98ed84ea98fb1067f97c9ae9eea9f9b49365357fecf000fb102a70dd09586
3
+ metadata.gz: ff70659073539f4ad6104e96ed6c9a85f4b639cf30e6bfdb6e56166d1e5cdf1d
4
+ data.tar.gz: c96bff5a56ef9f74fc011d649a3be81cbba192c3a3fabbac13281d48929d2d11
5
5
  SHA512:
6
- metadata.gz: e1fe26478d449c92f136e39142c1fde6db0261c171df1e45bd3e7a2e815065d09c00199c0dbdb93b88e39763da9a13a0909e7bb7f66da5c153acd003f653c990
7
- data.tar.gz: 74f7c89b22522e2a43f097d064d5e17785ff91fc042a9db39c3350b142c1a059fe5fade168dd77b62b09c458a5fcc359f68d85d28422fb46f90e69a48b91336d
6
+ metadata.gz: a3ba72b2e7fd8de71f8276858c8b81ef3767203c6769ab456d837bb9b1dc94f78c6a2584308582dcdf439e48c11d098d6d3673aecc0e34dd01775a5f9263334d
7
+ data.tar.gz: eddfebd33fbce4ffd79eb80b33d7715e46706947973b4c406b7d866ff98902de24e9bd8ddaa7addf91c6fad6d071aed195f0dbd36800e59a3fbc832feb5e10cd
@@ -19,7 +19,7 @@ module CollavreOpenclaw
19
19
  adapter_class = self.class.adapter_registry[normalized_vendor]
20
20
 
21
21
  if adapter_class
22
- # Use the custom adapter
22
+ # Use the custom adapter (tools not supported for OpenClaw)
23
23
  user = context&.dig(:user)
24
24
  adapter = adapter_class.new(
25
25
  user: user,
@@ -31,15 +31,14 @@ module CollavreOpenclaw
31
31
  error_message = nil
32
32
 
33
33
  begin
34
- response_content = adapter.chat(contents, tools: tools, &block)
34
+ response_content = adapter.chat(contents, &block)
35
35
  rescue StandardError => e
36
36
  error_message = e.message
37
37
  raise
38
38
  ensure
39
- # Log the interaction just like AiClient does
40
39
  log_interaction(
41
40
  messages: Array(contents),
42
- tools: tools,
41
+ tools: [],
43
42
  response_content: response_content,
44
43
  error_message: error_message,
45
44
  input_tokens: nil,
@@ -0,0 +1,192 @@
1
+ module CollavreOpenclaw
2
+ # Manages WebSocket connections to OpenClaw Gateways.
3
+ #
4
+ # Singleton: use ConnectionManager.instance
5
+ #
6
+ # Features:
7
+ # - Lazy connect (creates connection on first use)
8
+ # - Connection sharing (same gateway_url → same connection)
9
+ # - Idle timeout (disconnects after inactivity)
10
+ # - Thread-safe access
11
+ # - Graceful shutdown
12
+ #
13
+ # Multiple AI agents using the same Gateway share a single WebSocket
14
+ # connection, reducing resource usage and connection overhead.
15
+ class ConnectionManager
16
+ include Singleton
17
+
18
+ def initialize
19
+ @connections = {} # gateway_url → WebsocketClient
20
+ @gateway_users = {} # gateway_url → Set<user_id> (users sharing this gateway)
21
+ @user_gateways = {} # user_id → gateway_url (reverse lookup)
22
+ @mutex = Mutex.new
23
+ @idle_checker = nil
24
+ @proactive_handler = nil
25
+ @proactive_message_handler = ProactiveMessageHandler.new
26
+ setup_default_proactive_handler!
27
+ start_idle_checker!
28
+ end
29
+
30
+ # Get or create a WebSocket connection for a user.
31
+ # Users with the same gateway_url share a single connection.
32
+ #
33
+ # @param user [User] must respond to #gateway_url and #llm_api_key
34
+ # @return [WebsocketClient]
35
+ def connection_for(user)
36
+ gateway_url = user.gateway_url.to_s.strip
37
+ if gateway_url.blank?
38
+ Rails.logger.warn("[CollavreOpenclaw::ConnectionManager] User #{user.id} has blank gateway_url, cannot create connection")
39
+ return nil
40
+ end
41
+
42
+ @mutex.synchronize do
43
+ client = @connections[gateway_url]
44
+
45
+ if client.nil?
46
+ client = WebsocketClient.new(user: user)
47
+ client.on_proactive_message(&@proactive_handler) if @proactive_handler
48
+ @connections[gateway_url] = client
49
+ @gateway_users[gateway_url] = Set.new
50
+ end
51
+
52
+ # Track this user as using this gateway
53
+ @gateway_users[gateway_url].add(user.id)
54
+ @user_gateways[user.id] = gateway_url
55
+
56
+ client
57
+ end
58
+ end
59
+
60
+ # Disconnect a specific user from their gateway connection.
61
+ # The connection is only closed if no other users are sharing it.
62
+ def disconnect(user)
63
+ @mutex.synchronize do
64
+ gateway_url = @user_gateways.delete(user.id)
65
+ return unless gateway_url
66
+
67
+ users = @gateway_users[gateway_url]
68
+ users&.delete(user.id)
69
+
70
+ # Only disconnect if no users are sharing this gateway
71
+ if users.nil? || users.empty?
72
+ client = @connections.delete(gateway_url)
73
+ @gateway_users.delete(gateway_url)
74
+ client&.disconnect!
75
+ end
76
+ end
77
+ end
78
+
79
+ # Disconnect all connections (for app shutdown)
80
+ def disconnect_all
81
+ @mutex.synchronize do
82
+ @connections.each_value(&:disconnect!)
83
+ @connections.clear
84
+ @gateway_users.clear
85
+ @user_gateways.clear
86
+ end
87
+ stop_idle_checker!
88
+ end
89
+
90
+ # Number of active connections
91
+ def connected_count
92
+ @mutex.synchronize do
93
+ @connections.count { |_, c| c.connected? }
94
+ end
95
+ end
96
+
97
+ # Status summary
98
+ def status
99
+ @mutex.synchronize do
100
+ states = @connections.values.group_by(&:state)
101
+ {
102
+ connected: states[:connected]&.size || 0,
103
+ connecting: states[:connecting]&.size || 0,
104
+ reconnecting: states[:reconnecting]&.size || 0,
105
+ disconnected: states[:disconnected]&.size || 0,
106
+ total_connections: @connections.size,
107
+ total_users: @user_gateways.size
108
+ }
109
+ end
110
+ end
111
+
112
+ # Register a proactive message handler for all connections.
113
+ # New connections will also get this handler.
114
+ def on_proactive_message(&handler)
115
+ @mutex.synchronize do
116
+ @proactive_handler = handler
117
+ @connections.each_value do |client|
118
+ client.on_proactive_message(&handler)
119
+ end
120
+ end
121
+ end
122
+
123
+ # Get the number of users sharing a specific gateway
124
+ def users_for_gateway(gateway_url)
125
+ @mutex.synchronize do
126
+ @gateway_users[gateway_url]&.size || 0
127
+ end
128
+ end
129
+
130
+ # Check if a user has an active connection
131
+ def user_connected?(user)
132
+ @mutex.synchronize do
133
+ gateway_url = @user_gateways[user.id]
134
+ return false unless gateway_url
135
+ @connections[gateway_url]&.connected? || false
136
+ end
137
+ end
138
+
139
+ private
140
+
141
+ # Set up the default proactive message handler that dispatches
142
+ # unsolicited chat events to CallbackProcessorJob.
143
+ def setup_default_proactive_handler!
144
+ handler = @proactive_message_handler
145
+ on_proactive_message do |user, payload|
146
+ handler.handle(user, payload)
147
+ end
148
+ end
149
+
150
+ def start_idle_checker!
151
+ @idle_checker_stop = false
152
+ @idle_checker = Thread.new do
153
+ Thread.current.name = "openclaw-idle-checker"
154
+ loop do
155
+ break if @idle_checker_stop
156
+ sleep 1 # Check stop flag every second
157
+ @idle_check_counter = (@idle_check_counter || 0) + 1
158
+ next unless @idle_check_counter >= 60 # Run check every ~60 seconds
159
+ @idle_check_counter = 0
160
+ check_idle_connections!
161
+ rescue => e
162
+ Rails.logger.error("[CollavreOpenclaw::ConnectionManager] Idle checker error: #{e.message}")
163
+ end
164
+ end
165
+ end
166
+
167
+ def stop_idle_checker!
168
+ @idle_checker_stop = true
169
+ @idle_checker&.join(5)
170
+ @idle_checker = nil
171
+ end
172
+
173
+ def check_idle_connections!
174
+ timeout = CollavreOpenclaw.config.ws_idle_timeout
175
+
176
+ @mutex.synchronize do
177
+ idle_gateways = @connections.each_with_object([]) do |(gateway_url, client), urls|
178
+ urls << gateway_url if client.connected? && client.idle_seconds > timeout
179
+ end
180
+
181
+ idle_gateways.each do |gateway_url|
182
+ client = @connections.delete(gateway_url)
183
+ user_ids = @gateway_users.delete(gateway_url) || []
184
+ user_ids.each { |uid| @user_gateways.delete(uid) }
185
+ user_count = user_ids.size
186
+ Rails.logger.info("[CollavreOpenclaw::ConnectionManager] Disconnecting idle connection for gateway #{gateway_url} (#{user_count} users)")
187
+ client&.disconnect!
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,52 @@
1
+ require "eventmachine"
2
+
3
+ module CollavreOpenclaw
4
+ # Manages a single EventMachine reactor thread shared by all WebSocket connections.
5
+ # Thread-safe: multiple Rails threads can schedule work via .next_tick.
6
+ class EmReactor
7
+ class << self
8
+ def ensure_running!
9
+ @mutex ||= Mutex.new
10
+ @mutex.synchronize do
11
+ return if EM.reactor_running?
12
+
13
+ @thread = Thread.new do
14
+ Thread.current.name = "openclaw-em-reactor"
15
+ EM.run
16
+ end
17
+
18
+ # Wait until the reactor is actually running
19
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
20
+ until EM.reactor_running?
21
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
22
+ raise "EventMachine reactor failed to start within 5 seconds"
23
+ end
24
+ sleep 0.01
25
+ end
26
+ end
27
+ end
28
+
29
+ def stop!
30
+ @mutex&.synchronize do
31
+ return unless EM.reactor_running?
32
+
33
+ # Must stop EM from within the reactor thread
34
+ EM.next_tick { EM.stop }
35
+ @thread&.join(5)
36
+ @thread = nil
37
+ end
38
+ end
39
+
40
+ def running?
41
+ EM.reactor_running?
42
+ end
43
+
44
+ # Schedule a block to run in the EM reactor thread.
45
+ # Safe to call from any thread.
46
+ def next_tick(&block)
47
+ ensure_running!
48
+ EM.next_tick(&block)
49
+ end
50
+ end
51
+ end
52
+ end