@mindexec/cli 0.2.135 → 0.2.137

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.135",
3
+ "version": "0.2.137",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -184,6 +184,20 @@ async function main() {
184
184
  assert.ok(frame.metadata.frameSeq > 0, 'frameSeq should be positive');
185
185
  assert.equal(frame.metadata.mimeType, 'image/png');
186
186
  assert.ok(frame.payload.length > 0, 'payload should be non-empty');
187
+
188
+ const frameStatus = await fetchJson(`${bridge.baseUrl}/api/status?remoteFrames=ws`);
189
+ assert.equal(frameStatus.ok, true);
190
+ assert.ok(frameStatus.payload?.remoteFrameWs?.clientCount >= 1, JSON.stringify(frameStatus.payload?.remoteFrameWs));
191
+ assert.ok(frameStatus.payload?.remoteFrameWs?.framesSent >= 1, JSON.stringify(frameStatus.payload?.remoteFrameWs));
192
+ assert.equal(frameStatus.payload?.remoteFrameWs?.lastFrameDeviceId, device.deviceId);
193
+ assert.equal(frameStatus.payload?.remoteFrameWs?.lastFrameSeq, frame.metadata.frameSeq);
194
+ assert.ok(
195
+ frameStatus.payload?.remoteFrameWs?.clients?.some(client =>
196
+ Array.isArray(client.deviceIds)
197
+ && client.deviceIds.includes(device.deviceId)
198
+ && client.autoStartLive === true
199
+ && client.sent >= 1),
200
+ JSON.stringify(frameStatus.payload?.remoteFrameWs));
187
201
  ws.close();
188
202
 
189
203
  console.log('Remote frame WebSocket smoke OK');
@@ -8,6 +8,7 @@ import net from 'node:net';
8
8
  import os from 'node:os';
9
9
  import path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import { WebSocketServer } from 'ws';
11
12
 
12
13
  const BRIDGE_TOKEN = 'remote-registry-follower-smoke-token';
13
14
  const PAIR_TOKEN = 'remote-registry-follower-pair-token';
@@ -84,6 +85,7 @@ function createRegistryTarget({ endpoint, endpointCandidates, leaseId, active =
84
85
 
85
86
  function startFakeSupabase(getTarget) {
86
87
  const requests = [];
88
+ const realtimeClients = new Set();
87
89
  const server = createServer((req, res) => {
88
90
  const parsed = new URL(req.url || '/', 'http://127.0.0.1');
89
91
  requests.push({
@@ -108,6 +110,77 @@ function startFakeSupabase(getTarget) {
108
110
  res.writeHead(404, { 'Content-Type': 'application/json' });
109
111
  res.end(JSON.stringify({ error: 'not-found' }));
110
112
  });
113
+ const realtime = new WebSocketServer({ noServer: true });
114
+
115
+ realtime.on('connection', (ws, req) => {
116
+ realtimeClients.add(ws);
117
+ ws.on('message', data => {
118
+ const text = data.toString();
119
+ let message = null;
120
+ try {
121
+ message = JSON.parse(text);
122
+ } catch {
123
+ return;
124
+ }
125
+
126
+ if (message.event === 'phx_join') {
127
+ assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
128
+ assert.equal(String(req.headers.authorization || ''), `Bearer ${ACCESS_TOKEN}`);
129
+ assert.equal(message.payload?.access_token, ACCESS_TOKEN);
130
+ assert.deepEqual(
131
+ message.payload?.config?.postgres_changes?.[0],
132
+ {
133
+ event: '*',
134
+ schema: 'public',
135
+ table: 'remote_host_targets',
136
+ filter: `user_id=eq.${USER_ID}`
137
+ });
138
+ ws.remoteRegistryTopic = message.topic;
139
+ ws.send(JSON.stringify({
140
+ topic: message.topic,
141
+ event: 'phx_reply',
142
+ payload: {
143
+ status: 'ok',
144
+ response: {
145
+ postgres_changes: [
146
+ {
147
+ id: 1,
148
+ event: '*',
149
+ schema: 'public',
150
+ table: 'remote_host_targets'
151
+ }
152
+ ]
153
+ }
154
+ },
155
+ ref: message.ref,
156
+ join_ref: message.join_ref
157
+ }));
158
+ return;
159
+ }
160
+
161
+ if (message.event === 'heartbeat') {
162
+ ws.send(JSON.stringify({
163
+ topic: message.topic,
164
+ event: 'phx_reply',
165
+ payload: { status: 'ok', response: {} },
166
+ ref: message.ref
167
+ }));
168
+ }
169
+ });
170
+ ws.on('close', () => realtimeClients.delete(ws));
171
+ });
172
+
173
+ server.on('upgrade', (req, socket, head) => {
174
+ const parsed = new URL(req.url || '/', 'http://127.0.0.1');
175
+ if (parsed.pathname !== '/realtime/v1/websocket') {
176
+ socket.destroy();
177
+ return;
178
+ }
179
+
180
+ realtime.handleUpgrade(req, socket, head, ws => {
181
+ realtime.emit('connection', ws, req);
182
+ });
183
+ });
111
184
 
112
185
  return new Promise((resolve, reject) => {
113
186
  server.once('error', reject);
@@ -117,7 +190,41 @@ function startFakeSupabase(getTarget) {
117
190
  resolve({
118
191
  url: `http://127.0.0.1:${port}`,
119
192
  requests,
120
- stop: () => new Promise(done => server.close(done))
193
+ realtimeClients,
194
+ broadcastChange: (type = 'UPDATE') => {
195
+ for (const client of realtimeClients) {
196
+ if (client.readyState !== 1 || !client.remoteRegistryTopic) {
197
+ continue;
198
+ }
199
+
200
+ client.send(JSON.stringify({
201
+ topic: client.remoteRegistryTopic,
202
+ event: 'postgres_changes',
203
+ payload: {
204
+ ids: [1],
205
+ data: {
206
+ schema: 'public',
207
+ table: 'remote_host_targets',
208
+ commit_timestamp: new Date().toISOString(),
209
+ type,
210
+ record: getTarget(),
211
+ old_record: null
212
+ }
213
+ },
214
+ ref: null
215
+ }));
216
+ }
217
+ },
218
+ stop: () => new Promise(done => {
219
+ for (const client of realtimeClients) {
220
+ try {
221
+ client.close();
222
+ } catch {
223
+ // best effort only
224
+ }
225
+ }
226
+ realtime.close(() => server.close(done));
227
+ })
121
228
  });
122
229
  });
123
230
  });
@@ -140,8 +247,10 @@ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label
140
247
  WORKSPACE_PATH: workspacePath,
141
248
  MINDEXEC_AUTH_DATA_ROOT: authRoot,
142
249
  MINDEXEC_REMOTE_REGISTRY_FOLLOWER: follower ? '1' : '0',
143
- MINDEXEC_REMOTE_REGISTRY_POLL_MS: '500',
250
+ MINDEXEC_REMOTE_REGISTRY_POLL_MS: '10000',
144
251
  MINDEXEC_REMOTE_REGISTRY_FAST_RETRY_MS: '250',
252
+ MINDEXEC_REMOTE_REGISTRY_REALTIME_RECONNECT_MS: '250',
253
+ MINDEXEC_REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS: '100',
145
254
  SUPABASE_URL: supabaseUrl,
146
255
  SUPABASE_KEY,
147
256
  NO_COLOR: '1'
@@ -196,7 +305,7 @@ async function writeSession(authRoot) {
196
305
  }), 'utf8');
197
306
  }
198
307
 
199
- async function waitForManagedAgent(bridge, managerEndpoint, label) {
308
+ async function waitForManagedAgent(bridge, managerEndpoint, label, timeoutMs = 20000) {
200
309
  return await waitFor(async () => {
201
310
  const result = await fetchJson(`${bridge.baseUrl}/api/remote/agent/status`);
202
311
  const agent = result.payload;
@@ -205,7 +314,7 @@ async function waitForManagedAgent(bridge, managerEndpoint, label) {
205
314
  && String(agent.manager || '') === managerEndpoint
206
315
  ? agent
207
316
  : null;
208
- }, 20000, `${label} managed agent\n${bridge.details()}`);
317
+ }, timeoutMs, `${label} managed agent\n${bridge.details()}`);
209
318
  }
210
319
 
211
320
  async function waitForConnectedDevice(bridge, label) {
@@ -280,14 +389,18 @@ async function main() {
280
389
  assert.equal(firstAgent.manager, hostAEndpoint);
281
390
  assert.equal(firstAgent.managerCandidates?.[0], staleEndpoint, JSON.stringify(firstAgent.managerCandidates));
282
391
  await waitForConnectedDevice(hostA, 'host-a');
392
+ const firstStatus = await fetchJson(`${client.baseUrl}/api/status`);
393
+ assert.equal(firstStatus.payload?.remoteRegistryRealtime?.subscribed, true, JSON.stringify(firstStatus.payload?.remoteRegistryRealtime));
394
+ assert.ok(fakeSupabase.realtimeClients.size >= 1, 'expected LocalBridge realtime watcher to connect');
283
395
 
284
396
  currentTarget = createRegistryTarget({
285
397
  endpoint: hostBEndpoint,
286
398
  endpointCandidates: [hostBEndpoint],
287
399
  leaseId: 'lease-b'
288
400
  });
401
+ fakeSupabase.broadcastChange('UPDATE');
289
402
 
290
- const secondAgent = await waitForManagedAgent(client, hostBEndpoint, 'host-b');
403
+ const secondAgent = await waitForManagedAgent(client, hostBEndpoint, 'host-b', 9000);
291
404
  assert.equal(secondAgent.usingNpx, false, JSON.stringify(secondAgent));
292
405
  assert.match(String(secondAgent.launcher || ''), /mindexec-remote-fast/i);
293
406
  assert.equal(secondAgent.manager, hostBEndpoint);
@@ -299,6 +412,7 @@ async function main() {
299
412
  active: false,
300
413
  expires_at: new Date(Date.now() - 1000).toISOString()
301
414
  };
415
+ fakeSupabase.broadcastChange('UPDATE');
302
416
 
303
417
  await waitFor(async () => {
304
418
  const result = await fetchJson(`${client.baseUrl}/api/remote/agent/status`);
@@ -306,7 +420,10 @@ async function main() {
306
420
  && String(result.payload?.lastError || '') === 'registry-inactive'
307
421
  ? result.payload
308
422
  : null;
309
- }, 15000, `registry inactive stop\n${client.details()}`);
423
+ }, 9000, `registry inactive stop\n${client.details()}`);
424
+ const finalStatus = await fetchJson(`${client.baseUrl}/api/status`);
425
+ assert.ok(finalStatus.payload?.remoteRegistryRealtime?.changes >= 2, JSON.stringify(finalStatus.payload?.remoteRegistryRealtime));
426
+ assert.ok(finalStatus.payload?.remoteRegistryRealtime?.wakeups >= 2, JSON.stringify(finalStatus.payload?.remoteRegistryRealtime));
310
427
 
311
428
  assert.ok(fakeSupabase.requests.length >= 2, JSON.stringify(fakeSupabase.requests));
312
429
  console.log('Remote registry follower smoke OK');
package/server.js CHANGED
@@ -17,7 +17,7 @@ import multer from 'multer';
17
17
  import crypto from 'crypto';
18
18
  import sharp from 'sharp';
19
19
  import { createServer } from 'http';
20
- import { WebSocketServer } from 'ws';
20
+ import { WebSocket, WebSocketServer } from 'ws';
21
21
  import chokidar from 'chokidar';
22
22
  import Parser from 'web-tree-sitter';
23
23
  import { fileURLToPath } from 'url';
@@ -2128,6 +2128,35 @@ const remoteFrameWss = new WebSocketServer({ noServer: true });
2128
2128
  const wsClients = new Set();
2129
2129
  const remoteFrameClients = new Set();
2130
2130
  const shellJobs = new Map();
2131
+ let remoteFrameClientSeq = 0;
2132
+ const remoteFrameWsDiagnostics = {
2133
+ opened: 0,
2134
+ closed: 0,
2135
+ errors: 0,
2136
+ framesReceived: 0,
2137
+ framesSent: 0,
2138
+ framesDropped: 0,
2139
+ framesInvalid: 0,
2140
+ framesNoClients: 0,
2141
+ framesNoSubscribers: 0,
2142
+ autoStartRequested: 0,
2143
+ autoStartStarted: 0,
2144
+ autoStartSkipped: 0,
2145
+ lastOpenAt: '',
2146
+ lastCloseAt: '',
2147
+ lastErrorAt: '',
2148
+ lastFrameAt: '',
2149
+ lastFrameDeviceId: '',
2150
+ lastFrameSeq: 0,
2151
+ lastFrameBytes: 0,
2152
+ lastFrameMatchedClients: 0,
2153
+ lastFrameSentClients: 0,
2154
+ lastFrameDroppedClients: 0,
2155
+ lastAutoStartAt: '',
2156
+ lastAutoStartRequested: 0,
2157
+ lastAutoStartStarted: 0,
2158
+ lastAutoStartSkipped: 0
2159
+ };
2131
2160
  const REMOTE_FRAME_WS_AUTO_START_LIMIT = 120;
2132
2161
  const REMOTE_FRAME_WS_DEFAULT_FPS = 12;
2133
2162
  const REMOTE_FRAME_WS_DEFAULT_MAX_WIDTH = 960;
@@ -2308,9 +2337,20 @@ function maybeAutoStartRemoteFrameLiveStreams(ws, deviceIds = []) {
2308
2337
  }
2309
2338
 
2310
2339
  if (started.length > 0 || skipped.length > 0) {
2340
+ const now = new Date().toISOString();
2341
+ ws.remoteFrameLastAutoStartAt = now;
2342
+ ws.remoteFrameLastAutoStartStarted = started.length;
2343
+ ws.remoteFrameLastAutoStartSkipped = skipped.length;
2344
+ remoteFrameWsDiagnostics.autoStartRequested += Math.min(deviceIds.length, REMOTE_FRAME_WS_AUTO_START_LIMIT);
2345
+ remoteFrameWsDiagnostics.autoStartStarted += started.length;
2346
+ remoteFrameWsDiagnostics.autoStartSkipped += skipped.length;
2347
+ remoteFrameWsDiagnostics.lastAutoStartAt = now;
2348
+ remoteFrameWsDiagnostics.lastAutoStartRequested = Math.min(deviceIds.length, REMOTE_FRAME_WS_AUTO_START_LIMIT);
2349
+ remoteFrameWsDiagnostics.lastAutoStartStarted = started.length;
2350
+ remoteFrameWsDiagnostics.lastAutoStartSkipped = skipped.length;
2311
2351
  sendRemoteFrameClientJson(ws, {
2312
2352
  type: 'RemoteFrameLiveAutoStart',
2313
- timestamp: new Date().toISOString(),
2353
+ timestamp: now,
2314
2354
  requested: Math.min(deviceIds.length, REMOTE_FRAME_WS_AUTO_START_LIMIT),
2315
2355
  started,
2316
2356
  skipped: skipped.slice(0, 24),
@@ -2352,20 +2392,36 @@ function buildRemoteFrameBinaryPacket(frameEvent) {
2352
2392
  }
2353
2393
 
2354
2394
  function broadcastRemoteBinaryFrame(frameEvent) {
2395
+ const frame = frameEvent?.frame || {};
2396
+ remoteFrameWsDiagnostics.framesReceived += 1;
2397
+ remoteFrameWsDiagnostics.lastFrameAt = new Date().toISOString();
2398
+ remoteFrameWsDiagnostics.lastFrameDeviceId = String(frameEvent?.deviceId || frame.deviceId || '').trim();
2399
+ remoteFrameWsDiagnostics.lastFrameSeq = Number(frame.frameSeq || 0) || 0;
2400
+ remoteFrameWsDiagnostics.lastFrameBytes = Number(frameEvent?.byteLength || frameEvent?.payload?.length || 0) || 0;
2401
+ remoteFrameWsDiagnostics.lastFrameMatchedClients = 0;
2402
+ remoteFrameWsDiagnostics.lastFrameSentClients = 0;
2403
+ remoteFrameWsDiagnostics.lastFrameDroppedClients = 0;
2404
+
2355
2405
  if (remoteFrameClients.size === 0) {
2406
+ remoteFrameWsDiagnostics.framesNoClients += 1;
2356
2407
  return;
2357
2408
  }
2358
2409
 
2359
- const deviceId = String(frameEvent?.deviceId || frameEvent?.frame?.deviceId || '').trim();
2410
+ const deviceId = remoteFrameWsDiagnostics.lastFrameDeviceId;
2360
2411
  if (!deviceId) {
2412
+ remoteFrameWsDiagnostics.framesInvalid += 1;
2361
2413
  return;
2362
2414
  }
2363
2415
 
2364
2416
  const packet = buildRemoteFrameBinaryPacket(frameEvent);
2365
2417
  if (!packet) {
2418
+ remoteFrameWsDiagnostics.framesInvalid += 1;
2366
2419
  return;
2367
2420
  }
2368
2421
 
2422
+ let matched = 0;
2423
+ let sent = 0;
2424
+ let dropped = 0;
2369
2425
  for (const client of remoteFrameClients) {
2370
2426
  if (client.readyState !== 1) {
2371
2427
  continue;
@@ -2376,23 +2432,85 @@ function broadcastRemoteBinaryFrame(frameEvent) {
2376
2432
  continue;
2377
2433
  }
2378
2434
 
2435
+ matched += 1;
2379
2436
  if (client.bufferedAmount > 8 * 1024 * 1024) {
2380
2437
  client.remoteFrameDroppedCount = (client.remoteFrameDroppedCount || 0) + 1;
2438
+ client.remoteFrameLastDropAt = new Date().toISOString();
2439
+ remoteFrameWsDiagnostics.framesDropped += 1;
2440
+ dropped += 1;
2381
2441
  continue;
2382
2442
  }
2383
2443
 
2384
2444
  try {
2385
2445
  client.send(packet, { binary: true });
2386
2446
  client.remoteFrameSentCount = (client.remoteFrameSentCount || 0) + 1;
2447
+ client.remoteFrameLastSentAt = new Date().toISOString();
2448
+ client.remoteFrameLastDeviceId = deviceId;
2449
+ client.remoteFrameLastSeq = remoteFrameWsDiagnostics.lastFrameSeq;
2450
+ remoteFrameWsDiagnostics.framesSent += 1;
2451
+ sent += 1;
2387
2452
  } catch {
2388
2453
  client.remoteFrameDroppedCount = (client.remoteFrameDroppedCount || 0) + 1;
2454
+ client.remoteFrameLastDropAt = new Date().toISOString();
2455
+ remoteFrameWsDiagnostics.framesDropped += 1;
2456
+ dropped += 1;
2389
2457
  }
2390
2458
  }
2459
+
2460
+ remoteFrameWsDiagnostics.lastFrameMatchedClients = matched;
2461
+ remoteFrameWsDiagnostics.lastFrameSentClients = sent;
2462
+ remoteFrameWsDiagnostics.lastFrameDroppedClients = dropped;
2463
+ if (matched === 0) {
2464
+ remoteFrameWsDiagnostics.framesNoSubscribers += 1;
2465
+ }
2466
+ }
2467
+
2468
+ function serializeRemoteFrameWsDiagnostics() {
2469
+ const clients = [];
2470
+ for (const client of remoteFrameClients) {
2471
+ const deviceIds = client.remoteFrameDeviceIds instanceof Set
2472
+ ? Array.from(client.remoteFrameDeviceIds)
2473
+ : [];
2474
+ clients.push({
2475
+ id: client.remoteFrameClientId || '',
2476
+ readyState: client.readyState,
2477
+ connectedAt: client.remoteFrameConnectedAt || '',
2478
+ subscribedAt: client.remoteFrameSubscriptionUpdatedAt || '',
2479
+ deviceCount: deviceIds.length,
2480
+ deviceIds: deviceIds.slice(0, 24),
2481
+ autoStartLive: client.remoteFrameAutoStartLive === true,
2482
+ autoStartOptions: client.remoteFrameAutoStartOptions || null,
2483
+ sent: Number(client.remoteFrameSentCount || 0),
2484
+ dropped: Number(client.remoteFrameDroppedCount || 0),
2485
+ bufferedAmount: Number(client.bufferedAmount || 0),
2486
+ lastSentAt: client.remoteFrameLastSentAt || '',
2487
+ lastDropAt: client.remoteFrameLastDropAt || '',
2488
+ lastDeviceId: client.remoteFrameLastDeviceId || '',
2489
+ lastFrameSeq: Number(client.remoteFrameLastSeq || 0) || 0,
2490
+ lastAutoStartAt: client.remoteFrameLastAutoStartAt || '',
2491
+ lastAutoStartStarted: Number(client.remoteFrameLastAutoStartStarted || 0),
2492
+ lastAutoStartSkipped: Number(client.remoteFrameLastAutoStartSkipped || 0)
2493
+ });
2494
+ }
2495
+
2496
+ return {
2497
+ protocol: 'mindexec.remote.frames.binary.v1',
2498
+ path: '/api/remote/frames/ws',
2499
+ clientCount: remoteFrameClients.size,
2500
+ ...remoteFrameWsDiagnostics,
2501
+ clients
2502
+ };
2391
2503
  }
2392
2504
 
2393
2505
  remoteFrameWss.on('connection', (ws, req) => {
2394
2506
  remoteFrameClients.add(ws);
2395
2507
  ws.binaryType = 'arraybuffer';
2508
+ ws.remoteFrameClientId = `rfws-${++remoteFrameClientSeq}`;
2509
+ ws.remoteFrameConnectedAt = new Date().toISOString();
2510
+ ws.remoteFrameSentCount = 0;
2511
+ ws.remoteFrameDroppedCount = 0;
2512
+ remoteFrameWsDiagnostics.opened += 1;
2513
+ remoteFrameWsDiagnostics.lastOpenAt = ws.remoteFrameConnectedAt;
2396
2514
  try {
2397
2515
  const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
2398
2516
  updateRemoteFrameClientSubscription(ws, {
@@ -2416,12 +2534,21 @@ remoteFrameWss.on('connection', (ws, req) => {
2416
2534
  });
2417
2535
  }
2418
2536
  });
2419
- ws.on('close', () => remoteFrameClients.delete(ws));
2420
- ws.on('error', () => remoteFrameClients.delete(ws));
2537
+ ws.on('close', () => {
2538
+ remoteFrameClients.delete(ws);
2539
+ remoteFrameWsDiagnostics.closed += 1;
2540
+ remoteFrameWsDiagnostics.lastCloseAt = new Date().toISOString();
2541
+ });
2542
+ ws.on('error', () => {
2543
+ remoteFrameClients.delete(ws);
2544
+ remoteFrameWsDiagnostics.errors += 1;
2545
+ remoteFrameWsDiagnostics.lastErrorAt = new Date().toISOString();
2546
+ });
2421
2547
  sendRemoteFrameClientJson(ws, {
2422
2548
  type: 'RemoteFrameSocketReady',
2423
2549
  timestamp: new Date().toISOString(),
2424
- protocol: 'mindexec.remote.frames.binary.v1'
2550
+ protocol: 'mindexec.remote.frames.binary.v1',
2551
+ clientId: ws.remoteFrameClientId
2425
2552
  });
2426
2553
  });
2427
2554
 
@@ -3014,6 +3141,11 @@ const REMOTE_REGISTRY_FOLLOWER_ENABLED = !/^(0|false|no|off)$/i.test(String(proc
3014
3141
  const REMOTE_REGISTRY_FOLLOWER_POLL_MS = Math.max(1500, Number(process.env.MINDEXEC_REMOTE_REGISTRY_POLL_MS || 5000) || 5000);
3015
3142
  const REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS = Math.max(500, Number(process.env.MINDEXEC_REMOTE_REGISTRY_FAST_RETRY_MS || 1200) || 1200);
3016
3143
  const REMOTE_REGISTRY_FOLLOWER_MISSING_SESSION_STOP_COUNT = 3;
3144
+ const REMOTE_REGISTRY_REALTIME_ENABLED = REMOTE_REGISTRY_FOLLOWER_ENABLED
3145
+ && !/^(0|false|no|off)$/i.test(String(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME || 'true'));
3146
+ const REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS = Math.max(10000, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS || 25000) || 25000);
3147
+ const REMOTE_REGISTRY_REALTIME_RECONNECT_MS = Math.max(1000, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_RECONNECT_MS || 2500) || 2500);
3148
+ const REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS = Math.max(100, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS || 250) || 250);
3017
3149
  let remoteAgentState = createRemoteAgentIdleState();
3018
3150
  let remoteAgentSyncReportState = null;
3019
3151
  let remoteAgentSyncReportLogKey = '';
@@ -3026,6 +3158,15 @@ let remoteRegistryFollowerTimer = null;
3026
3158
  let remoteRegistryFollowerInFlight = false;
3027
3159
  let remoteRegistryFollowerStarted = false;
3028
3160
  let remoteRegistryFollowerConsecutiveMissingSession = 0;
3161
+ let remoteRegistryRealtimeSocket = null;
3162
+ let remoteRegistryRealtimeSessionKey = '';
3163
+ let remoteRegistryRealtimeTopic = '';
3164
+ let remoteRegistryRealtimeJoinRef = '';
3165
+ let remoteRegistryRealtimeRef = 0;
3166
+ let remoteRegistryRealtimeHeartbeatTimer = null;
3167
+ let remoteRegistryRealtimeReconnectTimer = null;
3168
+ let remoteRegistryRealtimeReconnectContext = null;
3169
+ let remoteRegistryRealtimeLastWakeAt = 0;
3029
3170
  let remoteRegistryFollowerState = {
3030
3171
  enabled: REMOTE_REGISTRY_FOLLOWER_ENABLED,
3031
3172
  status: REMOTE_REGISTRY_FOLLOWER_ENABLED ? 'idle' : 'disabled',
@@ -3039,6 +3180,26 @@ let remoteRegistryFollowerState = {
3039
3180
  targetNodeId: '',
3040
3181
  authenticated: false
3041
3182
  };
3183
+ let remoteRegistryRealtimeState = {
3184
+ enabled: REMOTE_REGISTRY_REALTIME_ENABLED,
3185
+ status: REMOTE_REGISTRY_REALTIME_ENABLED ? 'idle' : 'disabled',
3186
+ reason: '',
3187
+ connected: false,
3188
+ subscribed: false,
3189
+ userId: '',
3190
+ topic: '',
3191
+ lastOpenAt: '',
3192
+ lastJoinAt: '',
3193
+ lastMessageAt: '',
3194
+ lastChangeAt: '',
3195
+ lastCloseAt: '',
3196
+ lastErrorAt: '',
3197
+ lastError: '',
3198
+ reconnects: 0,
3199
+ messages: 0,
3200
+ changes: 0,
3201
+ wakeups: 0
3202
+ };
3042
3203
 
3043
3204
  function createRemoteAgentIdleState(overrides = {}) {
3044
3205
  return {
@@ -3490,6 +3651,11 @@ function isRemoteAgentProcessRunning() {
3490
3651
  && !proc.killed;
3491
3652
  }
3492
3653
 
3654
+ function isRemoteAgentRegistryOwned() {
3655
+ return isRemoteAgentProcessRunning()
3656
+ && /registry/i.test(String(remoteAgentState.source || ''));
3657
+ }
3658
+
3493
3659
  function isLocalRemoteHostTargetActive() {
3494
3660
  const status = remoteHub.getStatus({ includeSecrets: false });
3495
3661
  return status?.hostTargetActive === true
@@ -4290,6 +4456,424 @@ function updateRemoteRegistryFollowerState(patch = {}) {
4290
4456
  emitBridgeEvent('RemoteRegistryFollowerUpdated', serializeRemoteRegistryFollowerState());
4291
4457
  }
4292
4458
 
4459
+ function serializeRemoteRegistryRealtimeState() {
4460
+ return {
4461
+ ...remoteRegistryRealtimeState,
4462
+ heartbeatMs: REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS,
4463
+ reconnectMs: REMOTE_REGISTRY_REALTIME_RECONNECT_MS,
4464
+ debounceMs: REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS
4465
+ };
4466
+ }
4467
+
4468
+ function updateRemoteRegistryRealtimeState(patch = {}) {
4469
+ remoteRegistryRealtimeState = {
4470
+ ...remoteRegistryRealtimeState,
4471
+ ...patch,
4472
+ enabled: REMOTE_REGISTRY_REALTIME_ENABLED
4473
+ };
4474
+ emitBridgeEvent('RemoteRegistryRealtimeUpdated', serializeRemoteRegistryRealtimeState());
4475
+ }
4476
+
4477
+ function clearRemoteRegistryRealtimeTimers() {
4478
+ if (remoteRegistryRealtimeHeartbeatTimer) {
4479
+ clearInterval(remoteRegistryRealtimeHeartbeatTimer);
4480
+ remoteRegistryRealtimeHeartbeatTimer = null;
4481
+ }
4482
+ if (remoteRegistryRealtimeReconnectTimer) {
4483
+ clearTimeout(remoteRegistryRealtimeReconnectTimer);
4484
+ remoteRegistryRealtimeReconnectTimer = null;
4485
+ }
4486
+ }
4487
+
4488
+ function closeRemoteRegistryRealtime(reason = 'stopped') {
4489
+ clearRemoteRegistryRealtimeTimers();
4490
+ remoteRegistryRealtimeReconnectContext = null;
4491
+ const socket = remoteRegistryRealtimeSocket;
4492
+ remoteRegistryRealtimeSocket = null;
4493
+ remoteRegistryRealtimeSessionKey = '';
4494
+ remoteRegistryRealtimeTopic = '';
4495
+ remoteRegistryRealtimeJoinRef = '';
4496
+
4497
+ if (socket && socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) {
4498
+ try {
4499
+ socket.close(1000, String(reason || 'stopped').slice(0, 120));
4500
+ } catch {
4501
+ // Best-effort close only.
4502
+ }
4503
+ }
4504
+
4505
+ updateRemoteRegistryRealtimeState({
4506
+ status: REMOTE_REGISTRY_REALTIME_ENABLED ? 'stopped' : 'disabled',
4507
+ reason,
4508
+ connected: false,
4509
+ subscribed: false,
4510
+ userId: '',
4511
+ topic: ''
4512
+ });
4513
+ }
4514
+
4515
+ function buildRemoteRegistryRealtimeUrl(config) {
4516
+ try {
4517
+ const url = new URL('/realtime/v1/websocket', config.url);
4518
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
4519
+ url.searchParams.set('apikey', config.key);
4520
+ url.searchParams.set('vsn', '1.0.0');
4521
+ return url.toString();
4522
+ } catch {
4523
+ return '';
4524
+ }
4525
+ }
4526
+
4527
+ function getRemoteRegistryRealtimeSessionKey(config, session) {
4528
+ const tokenHash = crypto
4529
+ .createHash('sha256')
4530
+ .update(String(session?.accessToken || ''))
4531
+ .digest('hex')
4532
+ .slice(0, 16);
4533
+ return [
4534
+ String(config?.url || '').replace(/\/+$/, ''),
4535
+ String(session?.userId || ''),
4536
+ tokenHash
4537
+ ].join('|');
4538
+ }
4539
+
4540
+ function sendRemoteRegistryRealtimeMessage(socket, topic, event, payload = {}, options = {}) {
4541
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
4542
+ return '';
4543
+ }
4544
+
4545
+ const ref = String(++remoteRegistryRealtimeRef);
4546
+ const message = {
4547
+ topic,
4548
+ event,
4549
+ payload,
4550
+ ref
4551
+ };
4552
+ if (options.joinRef) {
4553
+ message.join_ref = options.joinRef;
4554
+ }
4555
+ socket.send(JSON.stringify(message));
4556
+ return ref;
4557
+ }
4558
+
4559
+ function sendRemoteRegistryRealtimeHeartbeat(socket) {
4560
+ sendRemoteRegistryRealtimeMessage(socket, 'phoenix', 'heartbeat', {});
4561
+ }
4562
+
4563
+ function parseRemoteRegistryRealtimeMessage(data) {
4564
+ let raw = data;
4565
+ if (Buffer.isBuffer(raw)) {
4566
+ raw = raw.toString('utf8');
4567
+ }
4568
+ if (ArrayBuffer.isView(raw)) {
4569
+ raw = Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString('utf8');
4570
+ }
4571
+
4572
+ const parsed = JSON.parse(String(raw || '{}'));
4573
+ if (Array.isArray(parsed)) {
4574
+ return {
4575
+ joinRef: parsed[0] ?? '',
4576
+ ref: parsed[1] ?? '',
4577
+ topic: parsed[2] ?? '',
4578
+ event: parsed[3] ?? '',
4579
+ payload: parsed[4] ?? {}
4580
+ };
4581
+ }
4582
+
4583
+ return {
4584
+ joinRef: parsed.join_ref ?? parsed.joinRef ?? '',
4585
+ ref: parsed.ref ?? '',
4586
+ topic: parsed.topic ?? '',
4587
+ event: parsed.event ?? '',
4588
+ payload: parsed.payload ?? {}
4589
+ };
4590
+ }
4591
+
4592
+ function isRemoteRegistryRealtimeChange(message) {
4593
+ if (!message || message.event !== 'postgres_changes') {
4594
+ return false;
4595
+ }
4596
+
4597
+ const data = message.payload?.data || message.payload;
4598
+ const table = String(data?.table || data?.record?.table || '').trim();
4599
+ const schema = String(data?.schema || data?.record?.schema || 'public').trim();
4600
+ return table === 'remote_host_targets' && (!schema || schema === 'public');
4601
+ }
4602
+
4603
+ function wakeRemoteRegistryFollowerFromRealtime(reason = 'realtime-change') {
4604
+ const now = Date.now();
4605
+ if (now - remoteRegistryRealtimeLastWakeAt < REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS) {
4606
+ return;
4607
+ }
4608
+
4609
+ remoteRegistryRealtimeLastWakeAt = now;
4610
+ updateRemoteRegistryRealtimeState({
4611
+ wakeups: remoteRegistryRealtimeState.wakeups + 1,
4612
+ lastChangeAt: new Date().toISOString(),
4613
+ reason
4614
+ });
4615
+ wakeRemoteRegistryFollower(reason).catch(error => {
4616
+ updateRemoteRegistryRealtimeState({
4617
+ status: 'error',
4618
+ reason: 'wake-failed',
4619
+ lastError: error?.message || String(error || ''),
4620
+ lastErrorAt: new Date().toISOString()
4621
+ });
4622
+ });
4623
+ }
4624
+
4625
+ function scheduleRemoteRegistryRealtimeReconnect(reason = 'reconnect') {
4626
+ if (!REMOTE_REGISTRY_REALTIME_ENABLED || isShuttingDown || !remoteRegistryRealtimeReconnectContext) {
4627
+ return;
4628
+ }
4629
+
4630
+ if (remoteRegistryRealtimeReconnectTimer) {
4631
+ return;
4632
+ }
4633
+
4634
+ updateRemoteRegistryRealtimeState({
4635
+ status: 'reconnecting',
4636
+ reason,
4637
+ connected: false,
4638
+ subscribed: false,
4639
+ reconnects: remoteRegistryRealtimeState.reconnects + 1
4640
+ });
4641
+ remoteRegistryRealtimeReconnectTimer = setTimeout(() => {
4642
+ remoteRegistryRealtimeReconnectTimer = null;
4643
+ const context = remoteRegistryRealtimeReconnectContext;
4644
+ if (!context || isShuttingDown) {
4645
+ return;
4646
+ }
4647
+ ensureRemoteRegistryRealtimeSubscription(context.config, context.session, 'realtime-reconnect')
4648
+ .catch(error => {
4649
+ updateRemoteRegistryRealtimeState({
4650
+ status: 'error',
4651
+ reason: 'reconnect-failed',
4652
+ lastError: error?.message || String(error || ''),
4653
+ lastErrorAt: new Date().toISOString()
4654
+ });
4655
+ scheduleRemoteRegistryRealtimeReconnect('reconnect-failed');
4656
+ });
4657
+ }, REMOTE_REGISTRY_REALTIME_RECONNECT_MS);
4658
+ remoteRegistryRealtimeReconnectTimer?.unref?.();
4659
+ }
4660
+
4661
+ async function ensureRemoteRegistryRealtimeSubscription(config, session, trigger = 'sync') {
4662
+ if (!REMOTE_REGISTRY_REALTIME_ENABLED) {
4663
+ closeRemoteRegistryRealtime('disabled');
4664
+ return serializeRemoteRegistryRealtimeState();
4665
+ }
4666
+
4667
+ if (!config?.url || !config?.key || !session?.accessToken || !session?.userId) {
4668
+ closeRemoteRegistryRealtime('not-authenticated');
4669
+ return serializeRemoteRegistryRealtimeState();
4670
+ }
4671
+
4672
+ const sessionKey = getRemoteRegistryRealtimeSessionKey(config, session);
4673
+ if (remoteRegistryRealtimeSocket
4674
+ && remoteRegistryRealtimeSessionKey === sessionKey
4675
+ && (remoteRegistryRealtimeSocket.readyState === WebSocket.OPEN
4676
+ || remoteRegistryRealtimeSocket.readyState === WebSocket.CONNECTING)) {
4677
+ return serializeRemoteRegistryRealtimeState();
4678
+ }
4679
+
4680
+ closeRemoteRegistryRealtime('replaced');
4681
+ const wsUrl = buildRemoteRegistryRealtimeUrl(config);
4682
+ if (!wsUrl) {
4683
+ updateRemoteRegistryRealtimeState({
4684
+ status: 'error',
4685
+ reason: 'invalid-realtime-url',
4686
+ lastError: 'invalid-realtime-url',
4687
+ lastErrorAt: new Date().toISOString()
4688
+ });
4689
+ return serializeRemoteRegistryRealtimeState();
4690
+ }
4691
+
4692
+ const topic = `realtime:remote-host-targets-${String(session.userId).replace(/[^a-zA-Z0-9]/g, '') || 'user'}`;
4693
+ const socket = new WebSocket(wsUrl, {
4694
+ headers: {
4695
+ apikey: config.key,
4696
+ Authorization: `Bearer ${session.accessToken}`
4697
+ }
4698
+ });
4699
+
4700
+ remoteRegistryRealtimeSocket = socket;
4701
+ remoteRegistryRealtimeSessionKey = sessionKey;
4702
+ remoteRegistryRealtimeTopic = topic;
4703
+ remoteRegistryRealtimeReconnectContext = {
4704
+ config: { url: config.url, key: config.key },
4705
+ session: {
4706
+ accessToken: session.accessToken,
4707
+ userId: session.userId,
4708
+ expiresAtMs: session.expiresAtMs
4709
+ }
4710
+ };
4711
+
4712
+ updateRemoteRegistryRealtimeState({
4713
+ status: 'connecting',
4714
+ reason: trigger,
4715
+ connected: false,
4716
+ subscribed: false,
4717
+ userId: session.userId,
4718
+ topic,
4719
+ lastError: ''
4720
+ });
4721
+
4722
+ socket.on('open', () => {
4723
+ if (remoteRegistryRealtimeSocket !== socket) {
4724
+ return;
4725
+ }
4726
+
4727
+ const now = new Date().toISOString();
4728
+ remoteRegistryRealtimeJoinRef = String(++remoteRegistryRealtimeRef);
4729
+ updateRemoteRegistryRealtimeState({
4730
+ status: 'joining',
4731
+ reason: 'socket-open',
4732
+ connected: true,
4733
+ subscribed: false,
4734
+ lastOpenAt: now,
4735
+ lastJoinAt: now
4736
+ });
4737
+ socket.send(JSON.stringify({
4738
+ topic,
4739
+ event: 'phx_join',
4740
+ payload: {
4741
+ config: {
4742
+ broadcast: { ack: false, self: false },
4743
+ presence: { enabled: false },
4744
+ postgres_changes: [
4745
+ {
4746
+ event: '*',
4747
+ schema: 'public',
4748
+ table: 'remote_host_targets',
4749
+ filter: `user_id=eq.${session.userId}`
4750
+ }
4751
+ ],
4752
+ private: false
4753
+ },
4754
+ access_token: session.accessToken
4755
+ },
4756
+ ref: remoteRegistryRealtimeJoinRef,
4757
+ join_ref: remoteRegistryRealtimeJoinRef
4758
+ }));
4759
+
4760
+ if (remoteRegistryRealtimeHeartbeatTimer) {
4761
+ clearInterval(remoteRegistryRealtimeHeartbeatTimer);
4762
+ }
4763
+ remoteRegistryRealtimeHeartbeatTimer = setInterval(
4764
+ () => sendRemoteRegistryRealtimeHeartbeat(socket),
4765
+ REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS);
4766
+ remoteRegistryRealtimeHeartbeatTimer?.unref?.();
4767
+ });
4768
+
4769
+ socket.on('message', data => {
4770
+ if (remoteRegistryRealtimeSocket !== socket) {
4771
+ return;
4772
+ }
4773
+
4774
+ let message = null;
4775
+ try {
4776
+ message = parseRemoteRegistryRealtimeMessage(data);
4777
+ } catch (error) {
4778
+ updateRemoteRegistryRealtimeState({
4779
+ status: 'error',
4780
+ reason: 'message-parse-failed',
4781
+ lastError: error?.message || String(error || ''),
4782
+ lastErrorAt: new Date().toISOString()
4783
+ });
4784
+ return;
4785
+ }
4786
+
4787
+ updateRemoteRegistryRealtimeState({
4788
+ messages: remoteRegistryRealtimeState.messages + 1,
4789
+ lastMessageAt: new Date().toISOString()
4790
+ });
4791
+
4792
+ if (message.event === 'phx_reply' && message.ref === remoteRegistryRealtimeJoinRef) {
4793
+ const ok = String(message.payload?.status || '').toLowerCase() === 'ok';
4794
+ updateRemoteRegistryRealtimeState({
4795
+ status: ok ? 'subscribed' : 'error',
4796
+ reason: ok ? 'joined' : 'join-failed',
4797
+ subscribed: ok,
4798
+ lastError: ok ? '' : JSON.stringify(message.payload || {}).slice(0, 240),
4799
+ lastErrorAt: ok ? remoteRegistryRealtimeState.lastErrorAt : new Date().toISOString()
4800
+ });
4801
+ return;
4802
+ }
4803
+
4804
+ if (message.event === 'system') {
4805
+ const status = String(message.payload?.status || '').toLowerCase();
4806
+ if (status === 'ok') {
4807
+ updateRemoteRegistryRealtimeState({
4808
+ status: 'subscribed',
4809
+ reason: 'system-ok',
4810
+ subscribed: true
4811
+ });
4812
+ } else if (status === 'error' || status === 'timeout') {
4813
+ updateRemoteRegistryRealtimeState({
4814
+ status: 'error',
4815
+ reason: 'system-error',
4816
+ subscribed: false,
4817
+ lastError: String(message.payload?.message || status).slice(0, 240),
4818
+ lastErrorAt: new Date().toISOString()
4819
+ });
4820
+ }
4821
+ return;
4822
+ }
4823
+
4824
+ if (message.event === 'phx_error' || message.event === 'phx_close') {
4825
+ updateRemoteRegistryRealtimeState({
4826
+ status: 'error',
4827
+ reason: message.event,
4828
+ subscribed: false,
4829
+ lastError: message.event,
4830
+ lastErrorAt: new Date().toISOString()
4831
+ });
4832
+ scheduleRemoteRegistryRealtimeReconnect(message.event);
4833
+ return;
4834
+ }
4835
+
4836
+ if (isRemoteRegistryRealtimeChange(message)) {
4837
+ updateRemoteRegistryRealtimeState({
4838
+ changes: remoteRegistryRealtimeState.changes + 1
4839
+ });
4840
+ wakeRemoteRegistryFollowerFromRealtime('realtime-change');
4841
+ }
4842
+ });
4843
+
4844
+ socket.on('close', (code, reasonBuffer) => {
4845
+ if (remoteRegistryRealtimeSocket !== socket) {
4846
+ return;
4847
+ }
4848
+
4849
+ clearRemoteRegistryRealtimeTimers();
4850
+ remoteRegistryRealtimeSocket = null;
4851
+ updateRemoteRegistryRealtimeState({
4852
+ status: 'closed',
4853
+ reason: reasonBuffer?.toString?.() || `close:${code}`,
4854
+ connected: false,
4855
+ subscribed: false,
4856
+ lastCloseAt: new Date().toISOString()
4857
+ });
4858
+ scheduleRemoteRegistryRealtimeReconnect(`close:${code}`);
4859
+ });
4860
+
4861
+ socket.on('error', error => {
4862
+ if (remoteRegistryRealtimeSocket !== socket) {
4863
+ return;
4864
+ }
4865
+
4866
+ updateRemoteRegistryRealtimeState({
4867
+ status: 'error',
4868
+ reason: 'socket-error',
4869
+ lastError: error?.message || String(error || ''),
4870
+ lastErrorAt: new Date().toISOString()
4871
+ });
4872
+ });
4873
+
4874
+ return serializeRemoteRegistryRealtimeState();
4875
+ }
4876
+
4293
4877
  function readSupabaseRuntimeConfig() {
4294
4878
  const envUrl = String(process.env.SUPABASE_URL || process.env.MINDEXEC_SUPABASE_URL || '').trim();
4295
4879
  const envKey = String(
@@ -4516,6 +5100,7 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
4516
5100
  try {
4517
5101
  const config = readSupabaseRuntimeConfig();
4518
5102
  if (!config.url || !config.key) {
5103
+ closeRemoteRegistryRealtime('supabase-config-missing');
4519
5104
  updateRemoteRegistryFollowerState({
4520
5105
  status: 'skipped',
4521
5106
  reason: 'supabase-config-missing',
@@ -4537,9 +5122,10 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
4537
5122
  const sessionPayload = await readStableAuthSessionPayload();
4538
5123
  const session = sessionPayload.found ? parseSupabaseSessionForRegistry(sessionPayload.content) : null;
4539
5124
  if (!session) {
5125
+ closeRemoteRegistryRealtime('registry-not-authenticated');
4540
5126
  remoteRegistryFollowerConsecutiveMissingSession += 1;
4541
5127
  if (remoteRegistryFollowerConsecutiveMissingSession >= REMOTE_REGISTRY_FOLLOWER_MISSING_SESSION_STOP_COUNT
4542
- && isRemoteAgentProcessRunning()) {
5128
+ && isRemoteAgentRegistryOwned()) {
4543
5129
  await stopRemoteAgentConnection('registry-not-authenticated');
4544
5130
  }
4545
5131
 
@@ -4567,6 +5153,7 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
4567
5153
 
4568
5154
  remoteRegistryFollowerConsecutiveMissingSession = 0;
4569
5155
  if (session.expiresAtMs && session.expiresAtMs <= Date.now()) {
5156
+ closeRemoteRegistryRealtime('session-expired');
4570
5157
  updateRemoteRegistryFollowerState({
4571
5158
  status: 'skipped',
4572
5159
  reason: 'session-expired',
@@ -4586,6 +5173,7 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
4586
5173
  }
4587
5174
 
4588
5175
  const localHub = remoteHub.getStatus({ includeSecrets: false });
5176
+ await ensureRemoteRegistryRealtimeSubscription(config, session, trigger);
4589
5177
  const target = await fetchRemoteRegistryTarget(config, session);
4590
5178
 
4591
5179
  if (localHub?.hostTargetActive === true) {
@@ -4652,7 +5240,7 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
4652
5240
  }
4653
5241
 
4654
5242
  if (!target?.active || isRemoteRegistryTargetExpired(target)) {
4655
- if (isRemoteAgentProcessRunning()) {
5243
+ if (isRemoteAgentRegistryOwned()) {
4656
5244
  await stopRemoteAgentConnection('registry-inactive');
4657
5245
  }
4658
5246
  updateRemoteRegistryFollowerState({
@@ -9286,8 +9874,10 @@ app.get('/api/status', async (req, res) => {
9286
9874
  bridgeTokenHeader,
9287
9875
  bridgeAuthRequired,
9288
9876
  remoteHub: remoteHub.getStatus({ includeSecrets: false }),
9877
+ remoteFrameWs: serializeRemoteFrameWsDiagnostics(),
9289
9878
  remoteAgent: serializeRemoteAgentState(),
9290
9879
  remoteRegistryFollower: serializeRemoteRegistryFollowerState(),
9880
+ remoteRegistryRealtime: serializeRemoteRegistryRealtimeState(),
9291
9881
  shellJobsPath: '/api/shell/jobs',
9292
9882
  companyCore: {
9293
9883
  baseUrl: companyCoreBaseUrl,
@@ -11118,6 +11708,12 @@ async function shutdownBridge(signal) {
11118
11708
  // Ignore registry follower shutdown errors
11119
11709
  }
11120
11710
 
11711
+ try {
11712
+ closeRemoteRegistryRealtime('bridge-shutdown');
11713
+ } catch {
11714
+ // Ignore registry realtime shutdown errors
11715
+ }
11716
+
11121
11717
  try {
11122
11718
  await stopRemoteAgentConnection('bridge-shutdown');
11123
11719
  } catch {