@mindexec/cli 0.2.132 → 0.2.134

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.132",
3
+ "version": "0.2.134",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -21,7 +21,7 @@
21
21
  "scripts": {
22
22
  "start": "node launch-bridge.cjs",
23
23
  "dev": "node launch-bridge.cjs --watch",
24
- "test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
24
+ "test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-registry-follower-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
25
25
  "test:auth": "node scripts/auth-session-smoke.mjs",
26
26
  "test:remote": "node scripts/remote-hub-smoke.mjs",
27
27
  "test:remote:scale": "node scripts/remote-hub-scale-smoke.mjs",
@@ -30,6 +30,7 @@
30
30
  "test:remote:frame-ws": "node scripts/remote-frame-ws-smoke.mjs",
31
31
  "test:remote:agent-ws": "node scripts/remote-agent-ws-smoke.mjs",
32
32
  "test:remote:managed": "node scripts/remote-agent-managed-smoke.mjs",
33
+ "test:remote:registry": "node scripts/remote-registry-follower-smoke.mjs",
33
34
  "test:remote:package": "node scripts/remote-agent-package-smoke.mjs",
34
35
  "test:remote:live-rate": "node scripts/remote-fast-live-rate-smoke.mjs",
35
36
  "test:remote:mdm-browser": "node scripts/remote-fast-mdm-browser-smoke.mjs",
@@ -13,9 +13,9 @@ const BRIDGE_TOKEN = 'remote-fast-live-rate-smoke-token';
13
13
  const PAIR_TOKEN = 'remote-fast-live-rate-pair-token';
14
14
  const LEASE_ID = 'remote-fast-live-rate-lease';
15
15
  const LOCAL_BRIDGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
16
- const REQUESTED_FPS = Number(process.env.MINDEXEC_REMOTE_LIVE_RATE_REQUEST_FPS || 10);
16
+ const REQUESTED_FPS = Number(process.env.MINDEXEC_REMOTE_LIVE_RATE_REQUEST_FPS || 12);
17
17
  const SAMPLE_MS = Number(process.env.MINDEXEC_REMOTE_LIVE_RATE_SAMPLE_MS || 3500);
18
- const MIN_EFFECTIVE_FPS = Number(process.env.MINDEXEC_REMOTE_LIVE_RATE_MIN_FPS || 8);
18
+ const MIN_EFFECTIVE_FPS = Number(process.env.MINDEXEC_REMOTE_LIVE_RATE_MIN_FPS || 10);
19
19
 
20
20
  function wait(ms) {
21
21
  return new Promise(resolve => setTimeout(resolve, ms));
@@ -12,7 +12,7 @@ const BRIDGE_TOKEN = 'remote-fast-mdm-browser-smoke-token';
12
12
  const PAIR_TOKEN = 'remote-fast-mdm-browser-pair-token';
13
13
  const LEASE_ID = 'remote-fast-mdm-browser-lease';
14
14
  const LOCAL_BRIDGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
- const REQUESTED_FPS = Number(process.env.MINDEXEC_REMOTE_MDM_BROWSER_REQUEST_FPS || 10);
15
+ const REQUESTED_FPS = Number(process.env.MINDEXEC_REMOTE_MDM_BROWSER_REQUEST_FPS || 12);
16
16
 
17
17
  function wait(ms) {
18
18
  return new Promise(resolve => setTimeout(resolve, ms));
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { spawn } from 'node:child_process';
5
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
6
+ import { createServer } from 'node:http';
7
+ import net from 'node:net';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const BRIDGE_TOKEN = 'remote-registry-follower-smoke-token';
13
+ const PAIR_TOKEN = 'remote-registry-follower-pair-token';
14
+ const USER_ID = '11111111-2222-4333-8444-555555555555';
15
+ const SUPABASE_KEY = 'remote-registry-follower-key';
16
+ const ACCESS_TOKEN = 'remote-registry-follower-access-token';
17
+ const LOCAL_BRIDGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
+
19
+ function wait(ms) {
20
+ return new Promise(resolve => setTimeout(resolve, ms));
21
+ }
22
+
23
+ async function findFreePort() {
24
+ return await new Promise((resolve, reject) => {
25
+ const server = net.createServer();
26
+ server.unref();
27
+ server.once('error', reject);
28
+ server.listen(0, '127.0.0.1', () => {
29
+ const address = server.address();
30
+ const port = typeof address === 'object' && address ? address.port : 0;
31
+ server.close(() => resolve(port));
32
+ });
33
+ });
34
+ }
35
+
36
+ async function fetchJson(url, options = {}) {
37
+ const response = await fetch(url, {
38
+ ...options,
39
+ headers: {
40
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
41
+ 'X-Bridge-Token': options.token || BRIDGE_TOKEN,
42
+ ...(options.headers || {})
43
+ }
44
+ });
45
+ let payload = null;
46
+ try {
47
+ payload = await response.json();
48
+ } catch {
49
+ // Empty responses are allowed in failure diagnostics.
50
+ }
51
+ return { ok: response.ok, status: response.status, payload };
52
+ }
53
+
54
+ async function waitFor(predicate, timeoutMs, label) {
55
+ const startedAt = Date.now();
56
+ let lastError = null;
57
+ while (Date.now() - startedAt < timeoutMs) {
58
+ try {
59
+ const value = await predicate();
60
+ if (value) {
61
+ return value;
62
+ }
63
+ } catch (error) {
64
+ lastError = error;
65
+ }
66
+ await wait(100);
67
+ }
68
+ throw new Error(`Timed out waiting for ${label}${lastError ? `: ${lastError.message}` : ''}.`);
69
+ }
70
+
71
+ function createRegistryTarget({ endpoint, endpointCandidates, leaseId, active = true }) {
72
+ return {
73
+ user_id: USER_ID,
74
+ active,
75
+ endpoint,
76
+ endpoint_candidates: endpointCandidates,
77
+ pair_token: PAIR_TOKEN,
78
+ lease_id: leaseId,
79
+ node_id: 'remote-registry-follower-node',
80
+ host_instance_id: `host-${leaseId}`,
81
+ expires_at: new Date(Date.now() + 60_000).toISOString()
82
+ };
83
+ }
84
+
85
+ function startFakeSupabase(getTarget) {
86
+ const requests = [];
87
+ const server = createServer((req, res) => {
88
+ const parsed = new URL(req.url || '/', 'http://127.0.0.1');
89
+ requests.push({
90
+ method: req.method,
91
+ pathname: parsed.pathname,
92
+ authorization: req.headers.authorization || '',
93
+ apikey: req.headers.apikey || ''
94
+ });
95
+
96
+ if (req.method === 'GET' && parsed.pathname === '/rest/v1/remote_host_targets') {
97
+ assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
98
+ assert.equal(String(req.headers.authorization || ''), `Bearer ${ACCESS_TOKEN}`);
99
+ res.writeHead(200, {
100
+ 'Content-Type': 'application/json',
101
+ 'Cache-Control': 'no-store'
102
+ });
103
+ const target = getTarget();
104
+ res.end(JSON.stringify(target ? [target] : []));
105
+ return;
106
+ }
107
+
108
+ res.writeHead(404, { 'Content-Type': 'application/json' });
109
+ res.end(JSON.stringify({ error: 'not-found' }));
110
+ });
111
+
112
+ return new Promise((resolve, reject) => {
113
+ server.once('error', reject);
114
+ server.listen(0, '127.0.0.1', () => {
115
+ const address = server.address();
116
+ const port = typeof address === 'object' && address ? address.port : 0;
117
+ resolve({
118
+ url: `http://127.0.0.1:${port}`,
119
+ requests,
120
+ stop: () => new Promise(done => server.close(done))
121
+ });
122
+ });
123
+ });
124
+ }
125
+
126
+ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label, supabaseUrl = '', follower = false }) {
127
+ const child = spawn(process.execPath, ['server.js'], {
128
+ cwd: LOCAL_BRIDGE_DIR,
129
+ stdio: ['ignore', 'pipe', 'pipe'],
130
+ windowsHide: true,
131
+ env: {
132
+ ...process.env,
133
+ BRIDGE_PORT: String(bridgePort),
134
+ BRIDGE_TOKEN,
135
+ BRIDGE_REQUIRE_TOKEN: '1',
136
+ MINDEXEC_REMOTE_HUB: '1',
137
+ REMOTE_HUB_HOST: '127.0.0.1',
138
+ REMOTE_HUB_PORT: String(remoteHubPort),
139
+ REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
140
+ WORKSPACE_PATH: workspacePath,
141
+ MINDEXEC_AUTH_DATA_ROOT: authRoot,
142
+ MINDEXEC_REMOTE_REGISTRY_FOLLOWER: follower ? '1' : '0',
143
+ MINDEXEC_REMOTE_REGISTRY_POLL_MS: '500',
144
+ MINDEXEC_REMOTE_REGISTRY_FAST_RETRY_MS: '250',
145
+ SUPABASE_URL: supabaseUrl,
146
+ SUPABASE_KEY,
147
+ NO_COLOR: '1'
148
+ }
149
+ });
150
+
151
+ let stdout = '';
152
+ let stderr = '';
153
+ child.stdout.on('data', chunk => {
154
+ stdout += chunk.toString();
155
+ });
156
+ child.stderr.on('data', chunk => {
157
+ stderr += chunk.toString();
158
+ });
159
+
160
+ const exitPromise = new Promise(resolve => child.once('exit', resolve));
161
+ const details = () => `${label} stdout=${stdout}\n${label} stderr=${stderr}`;
162
+ const stop = async () => {
163
+ if (child.exitCode === null && !child.killed) {
164
+ child.kill('SIGTERM');
165
+ await Promise.race([exitPromise, wait(5000)]);
166
+ if (child.exitCode === null && !child.killed) {
167
+ child.kill('SIGKILL');
168
+ }
169
+ }
170
+ };
171
+
172
+ return {
173
+ baseUrl: `http://127.0.0.1:${bridgePort}`,
174
+ child,
175
+ details,
176
+ stop
177
+ };
178
+ }
179
+
180
+ async function waitForBridge(bridge) {
181
+ return await waitFor(async () => {
182
+ const result = await fetchJson(`${bridge.baseUrl}/api/status`);
183
+ return result.ok && result.payload?.status === 'ok' ? result.payload : null;
184
+ }, 30000, `bridge startup\n${bridge.details()}`);
185
+ }
186
+
187
+ async function writeSession(authRoot) {
188
+ await mkdir(authRoot, { recursive: true });
189
+ await writeFile(path.join(authRoot, 'supabase-session.json'), JSON.stringify({
190
+ access_token: ACCESS_TOKEN,
191
+ expires_at: Math.floor(Date.now() / 1000) + 3600,
192
+ user: {
193
+ id: USER_ID,
194
+ email: 'remote-registry-follower@example.test'
195
+ }
196
+ }), 'utf8');
197
+ }
198
+
199
+ async function waitForManagedAgent(bridge, managerEndpoint, label) {
200
+ return await waitFor(async () => {
201
+ const result = await fetchJson(`${bridge.baseUrl}/api/remote/agent/status`);
202
+ const agent = result.payload;
203
+ return agent?.running === true
204
+ && agent?.ready === true
205
+ && String(agent.manager || '') === managerEndpoint
206
+ ? agent
207
+ : null;
208
+ }, 20000, `${label} managed agent\n${bridge.details()}`);
209
+ }
210
+
211
+ async function waitForConnectedDevice(bridge, label) {
212
+ return await waitFor(async () => {
213
+ const result = await fetchJson(`${bridge.baseUrl}/api/remote/devices`);
214
+ return result.payload?.devices?.find(device => device.connected === true) || null;
215
+ }, 12000, `${label} connected device\n${bridge.details()}`);
216
+ }
217
+
218
+ async function main() {
219
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'mindexec-remote-registry-smoke-'));
220
+ let fakeSupabase = null;
221
+ let hostA = null;
222
+ let hostB = null;
223
+ let client = null;
224
+
225
+ const hostABridgePort = await findFreePort();
226
+ const hostARemotePort = await findFreePort();
227
+ const hostBBridgePort = await findFreePort();
228
+ const hostBRemotePort = await findFreePort();
229
+ const clientBridgePort = await findFreePort();
230
+ const clientRemotePort = await findFreePort();
231
+ const stalePort = await findFreePort();
232
+ const hostAEndpoint = `127.0.0.1:${hostARemotePort}`;
233
+ const hostBEndpoint = `127.0.0.1:${hostBRemotePort}`;
234
+ const staleEndpoint = `127.0.0.1:${stalePort}`;
235
+ let currentTarget = createRegistryTarget({
236
+ endpoint: hostAEndpoint,
237
+ endpointCandidates: [staleEndpoint, hostAEndpoint],
238
+ leaseId: 'lease-a'
239
+ });
240
+
241
+ try {
242
+ fakeSupabase = await startFakeSupabase(() => currentTarget);
243
+
244
+ const hostAAuth = path.join(tempRoot, 'auth-host-a');
245
+ const hostBAuth = path.join(tempRoot, 'auth-host-b');
246
+ const clientAuth = path.join(tempRoot, 'auth-client');
247
+ await writeSession(clientAuth);
248
+
249
+ hostA = spawnBridge({
250
+ bridgePort: hostABridgePort,
251
+ remoteHubPort: hostARemotePort,
252
+ workspacePath: path.join(tempRoot, 'host-a'),
253
+ authRoot: hostAAuth,
254
+ label: 'host-a'
255
+ });
256
+ hostB = spawnBridge({
257
+ bridgePort: hostBBridgePort,
258
+ remoteHubPort: hostBRemotePort,
259
+ workspacePath: path.join(tempRoot, 'host-b'),
260
+ authRoot: hostBAuth,
261
+ label: 'host-b'
262
+ });
263
+ await waitForBridge(hostA);
264
+ await waitForBridge(hostB);
265
+
266
+ client = spawnBridge({
267
+ bridgePort: clientBridgePort,
268
+ remoteHubPort: clientRemotePort,
269
+ workspacePath: path.join(tempRoot, 'client'),
270
+ authRoot: clientAuth,
271
+ label: 'client',
272
+ supabaseUrl: fakeSupabase.url,
273
+ follower: true
274
+ });
275
+ await waitForBridge(client);
276
+
277
+ const firstAgent = await waitForManagedAgent(client, hostAEndpoint, 'host-a');
278
+ assert.equal(firstAgent.usingNpx, false, JSON.stringify(firstAgent));
279
+ assert.match(String(firstAgent.launcher || ''), /mindexec-remote-fast/i);
280
+ assert.equal(firstAgent.manager, hostAEndpoint);
281
+ assert.equal(firstAgent.managerCandidates?.[0], staleEndpoint, JSON.stringify(firstAgent.managerCandidates));
282
+ await waitForConnectedDevice(hostA, 'host-a');
283
+
284
+ currentTarget = createRegistryTarget({
285
+ endpoint: hostBEndpoint,
286
+ endpointCandidates: [hostBEndpoint],
287
+ leaseId: 'lease-b'
288
+ });
289
+
290
+ const secondAgent = await waitForManagedAgent(client, hostBEndpoint, 'host-b');
291
+ assert.equal(secondAgent.usingNpx, false, JSON.stringify(secondAgent));
292
+ assert.match(String(secondAgent.launcher || ''), /mindexec-remote-fast/i);
293
+ assert.equal(secondAgent.manager, hostBEndpoint);
294
+ assert.equal(secondAgent.leaseId, 'lease-b');
295
+ await waitForConnectedDevice(hostB, 'host-b');
296
+
297
+ currentTarget = {
298
+ ...currentTarget,
299
+ active: false,
300
+ expires_at: new Date(Date.now() - 1000).toISOString()
301
+ };
302
+
303
+ await waitFor(async () => {
304
+ const result = await fetchJson(`${client.baseUrl}/api/remote/agent/status`);
305
+ return result.payload?.running === false
306
+ && String(result.payload?.lastError || '') === 'registry-inactive'
307
+ ? result.payload
308
+ : null;
309
+ }, 15000, `registry inactive stop\n${client.details()}`);
310
+
311
+ assert.ok(fakeSupabase.requests.length >= 2, JSON.stringify(fakeSupabase.requests));
312
+ console.log('Remote registry follower smoke OK');
313
+ } finally {
314
+ if (client) await client.stop();
315
+ if (hostB) await hostB.stop();
316
+ if (hostA) await hostA.stop();
317
+ if (fakeSupabase) await fakeSupabase.stop();
318
+ await rm(tempRoot, { recursive: true, force: true });
319
+ }
320
+ }
321
+
322
+ await main();
package/server.js CHANGED
@@ -41,7 +41,7 @@ const VERBOSE_CODEX_TRACE = /^(1|true|yes|on)$/i.test(String(process.env.BRIDGE_
41
41
  const COLOR_LOGS_ENABLED = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
42
42
  const DEFAULT_WEB_APP_ROOT = path.join(BRIDGE_ROOT, 'wwwroot');
43
43
 
44
- const ANSI = {
44
+ const ANSI = {
45
45
  reset: '\x1b[0m',
46
46
  bold: '\x1b[1m',
47
47
  dim: '\x1b[2m',
@@ -52,8 +52,10 @@ const ANSI = {
52
52
  blue: '\x1b[34m',
53
53
  magenta: '\x1b[35m',
54
54
  cyan: '\x1b[36m',
55
- white: '\x1b[37m'
56
- };
55
+ white: '\x1b[37m'
56
+ };
57
+
58
+ let isShuttingDown = false;
57
59
 
58
60
  function paint(text, ...styles) {
59
61
  const value = String(text ?? '');
@@ -2042,6 +2044,7 @@ app.post('/api/auth/session', async (req, res) => {
2042
2044
  try {
2043
2045
  const content = req.body?.content;
2044
2046
  await writeStableAuthSessionPayload(content);
2047
+ await wakeRemoteRegistryFollower('auth-session-saved');
2045
2048
  res.json({
2046
2049
  success: true
2047
2050
  });
@@ -2058,6 +2061,7 @@ app.post('/api/auth/session', async (req, res) => {
2058
2061
  app.delete('/api/auth/session', async (req, res) => {
2059
2062
  try {
2060
2063
  await deleteStableAuthSessionPayload();
2064
+ await wakeRemoteRegistryFollower('auth-session-cleared');
2061
2065
  res.json({
2062
2066
  success: true
2063
2067
  });
@@ -2912,6 +2916,10 @@ const REMOTE_AGENT_MAX_PARALLEL_CANDIDATES = 6;
2912
2916
  const REMOTE_AGENT_RECENT_MANAGER_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
2913
2917
  const REMOTE_AGENT_RECENT_MANAGER_CACHE_LIMIT = 64;
2914
2918
  const REMOTE_AGENT_RECENT_MANAGER_DEFAULT_KEY = 'default';
2919
+ const REMOTE_REGISTRY_FOLLOWER_ENABLED = !/^(0|false|no|off)$/i.test(String(process.env.MINDEXEC_REMOTE_REGISTRY_FOLLOWER || 'true'));
2920
+ const REMOTE_REGISTRY_FOLLOWER_POLL_MS = Math.max(1500, Number(process.env.MINDEXEC_REMOTE_REGISTRY_POLL_MS || 5000) || 5000);
2921
+ const REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS = Math.max(500, Number(process.env.MINDEXEC_REMOTE_REGISTRY_FAST_RETRY_MS || 1200) || 1200);
2922
+ const REMOTE_REGISTRY_FOLLOWER_MISSING_SESSION_STOP_COUNT = 3;
2915
2923
  let remoteAgentState = createRemoteAgentIdleState();
2916
2924
  let remoteAgentSyncReportState = null;
2917
2925
  let remoteAgentSyncReportLogKey = '';
@@ -2920,6 +2928,23 @@ let remoteAgentConnectPromise = null;
2920
2928
  const remoteAgentRecentSuccessfulManagers = new Map();
2921
2929
  let remoteAgentRecentSuccessfulManagersLoaded = false;
2922
2930
  let remoteAgentRecentSuccessfulManagersSavePromise = null;
2931
+ let remoteRegistryFollowerTimer = null;
2932
+ let remoteRegistryFollowerInFlight = false;
2933
+ let remoteRegistryFollowerStarted = false;
2934
+ let remoteRegistryFollowerConsecutiveMissingSession = 0;
2935
+ let remoteRegistryFollowerState = {
2936
+ enabled: REMOTE_REGISTRY_FOLLOWER_ENABLED,
2937
+ status: REMOTE_REGISTRY_FOLLOWER_ENABLED ? 'idle' : 'disabled',
2938
+ reason: '',
2939
+ lastAttemptAt: '',
2940
+ lastSuccessAt: '',
2941
+ lastError: '',
2942
+ targetEndpoint: '',
2943
+ targetEndpointCandidates: [],
2944
+ targetLeaseId: '',
2945
+ targetNodeId: '',
2946
+ authenticated: false
2947
+ };
2923
2948
 
2924
2949
  function createRemoteAgentIdleState(overrides = {}) {
2925
2950
  return {
@@ -4154,11 +4179,569 @@ async function startRemoteAgentConnectionAttempt(options = {}) {
4154
4179
  return { ok: true, alreadyRunning: false, agent: serializeRemoteAgentState() };
4155
4180
  }
4156
4181
 
4182
+ function serializeRemoteRegistryFollowerState() {
4183
+ return {
4184
+ ...remoteRegistryFollowerState,
4185
+ pollMs: REMOTE_REGISTRY_FOLLOWER_POLL_MS,
4186
+ fastRetryMs: REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS
4187
+ };
4188
+ }
4189
+
4190
+ function updateRemoteRegistryFollowerState(patch = {}) {
4191
+ remoteRegistryFollowerState = {
4192
+ ...remoteRegistryFollowerState,
4193
+ ...patch,
4194
+ enabled: REMOTE_REGISTRY_FOLLOWER_ENABLED
4195
+ };
4196
+ emitBridgeEvent('RemoteRegistryFollowerUpdated', serializeRemoteRegistryFollowerState());
4197
+ }
4198
+
4199
+ function readSupabaseRuntimeConfig() {
4200
+ const envUrl = String(process.env.SUPABASE_URL || process.env.MINDEXEC_SUPABASE_URL || '').trim();
4201
+ const envKey = String(
4202
+ process.env.SUPABASE_KEY
4203
+ || process.env.SUPABASE_ANON_KEY
4204
+ || process.env.SUPABASE_PUBLISHABLE_KEY
4205
+ || process.env.MINDEXEC_SUPABASE_KEY
4206
+ || '').trim();
4207
+ if (envUrl && envKey) {
4208
+ return { url: envUrl.replace(/\/+$/, ''), key: envKey };
4209
+ }
4210
+
4211
+ const candidates = [
4212
+ path.join(WEB_APP_ROOT || '', 'appsettings.json'),
4213
+ path.join(DEFAULT_WEB_APP_ROOT, 'appsettings.json'),
4214
+ path.resolve(BRIDGE_ROOT, '..', 'MindExecution.Web', 'wwwroot', 'appsettings.json')
4215
+ ].filter(Boolean);
4216
+
4217
+ for (const candidate of Array.from(new Set(candidates.map(item => path.resolve(item))))) {
4218
+ try {
4219
+ const payload = JSON.parse(readFileSync(candidate, 'utf8'));
4220
+ const url = String(payload?.Supabase?.Url || '').trim().replace(/\/+$/, '');
4221
+ const key = String(payload?.Supabase?.Key || payload?.Supabase?.AnonKey || payload?.Supabase?.PublishableKey || '').trim();
4222
+ if (url && key) {
4223
+ return { url, key };
4224
+ }
4225
+ } catch {
4226
+ // Try the next appsettings candidate.
4227
+ }
4228
+ }
4229
+
4230
+ return { url: '', key: '' };
4231
+ }
4232
+
4233
+ function readSupabaseSessionField(source, ...keys) {
4234
+ for (const key of keys) {
4235
+ const value = source?.[key];
4236
+ if (value !== undefined && value !== null && String(value).trim()) {
4237
+ return value;
4238
+ }
4239
+ }
4240
+ return '';
4241
+ }
4242
+
4243
+ function parseSupabaseSessionForRegistry(content) {
4244
+ let session = null;
4245
+ try {
4246
+ session = JSON.parse(String(content || ''));
4247
+ } catch {
4248
+ return null;
4249
+ }
4250
+
4251
+ const user = session?.user || session?.User || {};
4252
+ const accessToken = String(readSupabaseSessionField(session, 'access_token', 'accessToken', 'AccessToken')).trim();
4253
+ const userId = String(
4254
+ readSupabaseSessionField(user, 'id', 'Id')
4255
+ || readSupabaseSessionField(session, 'user_id', 'userId', 'UserId')).trim();
4256
+ const expiresAtRaw = Number(readSupabaseSessionField(session, 'expires_at', 'expiresAt', 'ExpiresAt') || 0);
4257
+ const expiresAtMs = Number.isFinite(expiresAtRaw) && expiresAtRaw > 0
4258
+ ? (expiresAtRaw > 9999999999 ? expiresAtRaw : expiresAtRaw * 1000)
4259
+ : 0;
4260
+
4261
+ if (!accessToken || !userId) {
4262
+ return null;
4263
+ }
4264
+
4265
+ return {
4266
+ accessToken,
4267
+ userId,
4268
+ expiresAtMs
4269
+ };
4270
+ }
4271
+
4272
+ function readRegistryTargetField(target, ...keys) {
4273
+ for (const key of keys) {
4274
+ const value = target?.[key];
4275
+ if (value !== undefined && value !== null) {
4276
+ return value;
4277
+ }
4278
+ }
4279
+ return '';
4280
+ }
4281
+
4282
+ function parseRegistryEndpointCandidates(value) {
4283
+ if (Array.isArray(value)) {
4284
+ return value.map(item => String(item || '').trim()).filter(Boolean);
4285
+ }
4286
+
4287
+ const text = String(value || '').trim();
4288
+ if (!text) {
4289
+ return [];
4290
+ }
4291
+
4292
+ try {
4293
+ const parsed = JSON.parse(text);
4294
+ if (Array.isArray(parsed)) {
4295
+ return parsed.map(item => String(item || '').trim()).filter(Boolean);
4296
+ }
4297
+ } catch {
4298
+ // Fall through to comma/newline split.
4299
+ }
4300
+
4301
+ return text.split(/[,\n]/g).map(item => item.trim()).filter(Boolean);
4302
+ }
4303
+
4304
+ function normalizeRemoteRegistryTarget(row) {
4305
+ if (!row || typeof row !== 'object') {
4306
+ return null;
4307
+ }
4308
+
4309
+ const activeRaw = readRegistryTargetField(row, 'active', 'Active');
4310
+ const active = activeRaw === true || /^(1|true|yes|on)$/i.test(String(activeRaw || ''));
4311
+ const endpoint = String(readRegistryTargetField(row, 'endpoint', 'Endpoint') || '').trim();
4312
+ const endpointCandidates = normalizeRemoteManagerEndpointList(
4313
+ parseRegistryEndpointCandidates(readRegistryTargetField(row, 'endpoint_candidates', 'endpointCandidates', 'EndpointCandidates')),
4314
+ endpoint);
4315
+ const pairToken = String(readRegistryTargetField(row, 'pair_token', 'pairToken', 'PairToken') || '').trim();
4316
+ const leaseId = String(readRegistryTargetField(row, 'lease_id', 'leaseId', 'LeaseId') || '').trim();
4317
+ const nodeId = String(readRegistryTargetField(row, 'node_id', 'nodeId', 'NodeId') || '').trim();
4318
+ const hostInstanceId = String(readRegistryTargetField(row, 'host_instance_id', 'hostInstanceId', 'HostInstanceId') || '').trim();
4319
+ const expiresAt = String(readRegistryTargetField(row, 'expires_at', 'expiresAt', 'ExpiresAt') || '').trim();
4320
+ const expiresAtMs = Date.parse(expiresAt);
4321
+
4322
+ return {
4323
+ active,
4324
+ endpoint,
4325
+ endpointCandidates,
4326
+ pairToken,
4327
+ leaseId,
4328
+ nodeId,
4329
+ hostInstanceId,
4330
+ expiresAt,
4331
+ expiresAtMs: Number.isFinite(expiresAtMs) ? expiresAtMs : 0
4332
+ };
4333
+ }
4334
+
4335
+ function isRemoteRegistryTargetExpired(target) {
4336
+ return !target?.expiresAtMs || target.expiresAtMs <= Date.now();
4337
+ }
4338
+
4339
+ function isRemoteRegistryTargetSameAsLocalHost(localHub, target) {
4340
+ const localLeaseId = safeRemoteAgentField(localHub?.hostTargetLeaseId, 128);
4341
+ const localHostInstanceId = safeRemoteAgentField(localHub?.hostTargetHostInstanceId || localHub?.hostInstanceId, 128);
4342
+ return !!localLeaseId
4343
+ && !!localHostInstanceId
4344
+ && !!target?.leaseId
4345
+ && !!target?.hostInstanceId
4346
+ && localLeaseId.toLowerCase() === String(target.leaseId).toLowerCase()
4347
+ && localHostInstanceId.toLowerCase() === String(target.hostInstanceId).toLowerCase();
4348
+ }
4349
+
4350
+ async function fetchRemoteRegistryTarget(config, session) {
4351
+ const url = new URL('/rest/v1/remote_host_targets', config.url);
4352
+ url.searchParams.set('select', '*');
4353
+ url.searchParams.set('user_id', `eq.${session.userId}`);
4354
+ url.searchParams.set('limit', '1');
4355
+
4356
+ const response = await fetch(url.toString(), {
4357
+ method: 'GET',
4358
+ headers: {
4359
+ apikey: config.key,
4360
+ Authorization: `Bearer ${session.accessToken}`,
4361
+ Accept: 'application/json'
4362
+ }
4363
+ });
4364
+
4365
+ if (!response.ok) {
4366
+ const text = await response.text().catch(() => '');
4367
+ const error = new Error(`registry-rest-${response.status}${text ? `:${shortenText(text, 160)}` : ''}`);
4368
+ error.statusCode = response.status;
4369
+ throw error;
4370
+ }
4371
+
4372
+ const payload = await response.json();
4373
+ return normalizeRemoteRegistryTarget(Array.isArray(payload) ? payload[0] : payload);
4374
+ }
4375
+
4376
+ async function reportRemoteRegistryFollowerSync(payload = {}) {
4377
+ const report = createRemoteAgentSyncReport(payload);
4378
+ rememberRemoteAgentSyncReport(report);
4379
+ return report;
4380
+ }
4381
+
4382
+ function scheduleRemoteRegistryFollower(delayMs = REMOTE_REGISTRY_FOLLOWER_POLL_MS, reason = 'timer') {
4383
+ if (!REMOTE_REGISTRY_FOLLOWER_ENABLED || isShuttingDown) {
4384
+ return;
4385
+ }
4386
+
4387
+ if (remoteRegistryFollowerTimer) {
4388
+ clearTimeout(remoteRegistryFollowerTimer);
4389
+ remoteRegistryFollowerTimer = null;
4390
+ }
4391
+
4392
+ remoteRegistryFollowerTimer = setTimeout(() => {
4393
+ remoteRegistryFollowerTimer = null;
4394
+ runRemoteRegistryFollowerOnce(reason).catch(error => {
4395
+ logWarn('remote', `registry follower failed: ${error?.message || error}`);
4396
+ updateRemoteRegistryFollowerState({
4397
+ status: 'error',
4398
+ reason: 'follower-error',
4399
+ lastError: error?.message || String(error || ''),
4400
+ lastAttemptAt: new Date().toISOString()
4401
+ });
4402
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS, 'error-retry');
4403
+ });
4404
+ }, Math.max(0, Number(delayMs) || 0));
4405
+ remoteRegistryFollowerTimer?.unref?.();
4406
+ }
4407
+
4408
+ async function wakeRemoteRegistryFollower(reason = 'wake') {
4409
+ if (!REMOTE_REGISTRY_FOLLOWER_ENABLED) {
4410
+ return;
4411
+ }
4412
+ scheduleRemoteRegistryFollower(0, reason);
4413
+ }
4414
+
4415
+ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
4416
+ if (!REMOTE_REGISTRY_FOLLOWER_ENABLED || remoteRegistryFollowerInFlight) {
4417
+ return serializeRemoteRegistryFollowerState();
4418
+ }
4419
+
4420
+ remoteRegistryFollowerInFlight = true;
4421
+ const attemptedAt = new Date().toISOString();
4422
+ try {
4423
+ const config = readSupabaseRuntimeConfig();
4424
+ if (!config.url || !config.key) {
4425
+ updateRemoteRegistryFollowerState({
4426
+ status: 'skipped',
4427
+ reason: 'supabase-config-missing',
4428
+ authenticated: false,
4429
+ lastAttemptAt: attemptedAt,
4430
+ lastError: ''
4431
+ });
4432
+ await reportRemoteRegistryFollowerSync({
4433
+ ok: false,
4434
+ skipped: true,
4435
+ reason: 'supabase-config-missing',
4436
+ trigger,
4437
+ authenticated: false
4438
+ });
4439
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'config-missing');
4440
+ return serializeRemoteRegistryFollowerState();
4441
+ }
4442
+
4443
+ const sessionPayload = await readStableAuthSessionPayload();
4444
+ const session = sessionPayload.found ? parseSupabaseSessionForRegistry(sessionPayload.content) : null;
4445
+ if (!session) {
4446
+ remoteRegistryFollowerConsecutiveMissingSession += 1;
4447
+ if (remoteRegistryFollowerConsecutiveMissingSession >= REMOTE_REGISTRY_FOLLOWER_MISSING_SESSION_STOP_COUNT
4448
+ && isRemoteAgentProcessRunning()) {
4449
+ await stopRemoteAgentConnection('registry-not-authenticated');
4450
+ }
4451
+
4452
+ updateRemoteRegistryFollowerState({
4453
+ status: 'skipped',
4454
+ reason: 'registry-not-authenticated',
4455
+ authenticated: false,
4456
+ lastAttemptAt: attemptedAt,
4457
+ lastError: '',
4458
+ targetEndpoint: '',
4459
+ targetEndpointCandidates: [],
4460
+ targetLeaseId: '',
4461
+ targetNodeId: ''
4462
+ });
4463
+ await reportRemoteRegistryFollowerSync({
4464
+ ok: true,
4465
+ skipped: true,
4466
+ reason: 'registry-not-authenticated',
4467
+ trigger,
4468
+ authenticated: false
4469
+ });
4470
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'auth-missing');
4471
+ return serializeRemoteRegistryFollowerState();
4472
+ }
4473
+
4474
+ remoteRegistryFollowerConsecutiveMissingSession = 0;
4475
+ if (session.expiresAtMs && session.expiresAtMs <= Date.now()) {
4476
+ updateRemoteRegistryFollowerState({
4477
+ status: 'skipped',
4478
+ reason: 'session-expired',
4479
+ authenticated: false,
4480
+ lastAttemptAt: attemptedAt,
4481
+ lastError: ''
4482
+ });
4483
+ await reportRemoteRegistryFollowerSync({
4484
+ ok: true,
4485
+ skipped: true,
4486
+ reason: 'session-expired',
4487
+ trigger,
4488
+ authenticated: false
4489
+ });
4490
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'session-expired');
4491
+ return serializeRemoteRegistryFollowerState();
4492
+ }
4493
+
4494
+ const localHub = remoteHub.getStatus({ includeSecrets: false });
4495
+ const target = await fetchRemoteRegistryTarget(config, session);
4496
+
4497
+ if (localHub?.hostTargetActive === true) {
4498
+ if (target?.active === true
4499
+ && !isRemoteRegistryTargetExpired(target)
4500
+ && !isRemoteRegistryTargetSameAsLocalHost(localHub, target)) {
4501
+ remoteHub.setHostTarget({
4502
+ enabled: false,
4503
+ nodeId: localHub.hostTargetNodeId
4504
+ });
4505
+ updateRemoteRegistryFollowerState({
4506
+ status: 'local-host-superseded',
4507
+ reason: 'local-host-superseded',
4508
+ authenticated: true,
4509
+ lastAttemptAt: attemptedAt,
4510
+ lastSuccessAt: attemptedAt,
4511
+ targetEndpoint: target.endpoint,
4512
+ targetEndpointCandidates: target.endpointCandidates,
4513
+ targetLeaseId: target.leaseId,
4514
+ targetNodeId: target.nodeId,
4515
+ lastError: ''
4516
+ });
4517
+ await reportRemoteRegistryFollowerSync({
4518
+ ok: true,
4519
+ attempted: true,
4520
+ disconnected: true,
4521
+ reason: 'local-host-superseded',
4522
+ trigger,
4523
+ authenticated: true,
4524
+ targetActive: true,
4525
+ targetEndpoint: target.endpoint,
4526
+ targetEndpointCandidates: target.endpointCandidates,
4527
+ targetLeaseId: target.leaseId,
4528
+ targetNodeId: target.nodeId,
4529
+ targetExpiresAt: target.expiresAt
4530
+ });
4531
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS, 'local-host-cleared');
4532
+ return serializeRemoteRegistryFollowerState();
4533
+ }
4534
+
4535
+ if (isRemoteAgentProcessRunning()) {
4536
+ await stopRemoteAgentConnection('local-host-target-active');
4537
+ }
4538
+ updateRemoteRegistryFollowerState({
4539
+ status: 'skipped',
4540
+ reason: 'local-monitor-is-host',
4541
+ authenticated: true,
4542
+ lastAttemptAt: attemptedAt,
4543
+ lastSuccessAt: attemptedAt,
4544
+ lastError: ''
4545
+ });
4546
+ await reportRemoteRegistryFollowerSync({
4547
+ ok: true,
4548
+ skipped: true,
4549
+ reason: 'local-monitor-is-host',
4550
+ trigger,
4551
+ authenticated: true,
4552
+ localHostTargetActive: true,
4553
+ localHostTargetLeaseId: localHub.hostTargetLeaseId,
4554
+ localHostTargetNodeId: localHub.hostTargetNodeId
4555
+ });
4556
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'local-host');
4557
+ return serializeRemoteRegistryFollowerState();
4558
+ }
4559
+
4560
+ if (!target?.active || isRemoteRegistryTargetExpired(target)) {
4561
+ if (isRemoteAgentProcessRunning()) {
4562
+ await stopRemoteAgentConnection('registry-inactive');
4563
+ }
4564
+ updateRemoteRegistryFollowerState({
4565
+ status: 'idle',
4566
+ reason: 'no-active-target',
4567
+ authenticated: true,
4568
+ lastAttemptAt: attemptedAt,
4569
+ lastSuccessAt: attemptedAt,
4570
+ targetEndpoint: '',
4571
+ targetEndpointCandidates: [],
4572
+ targetLeaseId: '',
4573
+ targetNodeId: '',
4574
+ lastError: ''
4575
+ });
4576
+ await reportRemoteRegistryFollowerSync({
4577
+ ok: true,
4578
+ skipped: true,
4579
+ reason: 'no-active-target',
4580
+ trigger,
4581
+ authenticated: true
4582
+ });
4583
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'no-target');
4584
+ return serializeRemoteRegistryFollowerState();
4585
+ }
4586
+
4587
+ if (isRemoteRegistryTargetSameAsLocalHost(localHub, target)) {
4588
+ updateRemoteRegistryFollowerState({
4589
+ status: 'skipped',
4590
+ reason: 'same-host-lease',
4591
+ authenticated: true,
4592
+ lastAttemptAt: attemptedAt,
4593
+ lastSuccessAt: attemptedAt,
4594
+ targetEndpoint: target.endpoint,
4595
+ targetEndpointCandidates: target.endpointCandidates,
4596
+ targetLeaseId: target.leaseId,
4597
+ targetNodeId: target.nodeId,
4598
+ lastError: ''
4599
+ });
4600
+ await reportRemoteRegistryFollowerSync({
4601
+ ok: true,
4602
+ skipped: true,
4603
+ reason: 'same-host-lease',
4604
+ trigger,
4605
+ authenticated: true,
4606
+ targetActive: true,
4607
+ targetEndpoint: target.endpoint,
4608
+ targetEndpointCandidates: target.endpointCandidates,
4609
+ targetLeaseId: target.leaseId,
4610
+ targetNodeId: target.nodeId,
4611
+ targetExpiresAt: target.expiresAt
4612
+ });
4613
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'same-host');
4614
+ return serializeRemoteRegistryFollowerState();
4615
+ }
4616
+
4617
+ if (target.endpointCandidates.length === 0 || !target.pairToken) {
4618
+ updateRemoteRegistryFollowerState({
4619
+ status: 'skipped',
4620
+ reason: 'target-missing-endpoint-or-pair',
4621
+ authenticated: true,
4622
+ lastAttemptAt: attemptedAt,
4623
+ lastSuccessAt: attemptedAt,
4624
+ targetEndpoint: target.endpoint,
4625
+ targetEndpointCandidates: target.endpointCandidates,
4626
+ targetLeaseId: target.leaseId,
4627
+ targetNodeId: target.nodeId,
4628
+ lastError: ''
4629
+ });
4630
+ await reportRemoteRegistryFollowerSync({
4631
+ ok: true,
4632
+ skipped: true,
4633
+ reason: 'target-missing-endpoint-or-pair',
4634
+ trigger,
4635
+ authenticated: true,
4636
+ targetActive: true,
4637
+ targetEndpoint: target.endpoint,
4638
+ targetEndpointCandidates: target.endpointCandidates,
4639
+ targetLeaseId: target.leaseId,
4640
+ targetNodeId: target.nodeId,
4641
+ targetExpiresAt: target.expiresAt
4642
+ });
4643
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'target-incomplete');
4644
+ return serializeRemoteRegistryFollowerState();
4645
+ }
4646
+
4647
+ const connect = await startRemoteAgentConnection({
4648
+ manager: target.endpointCandidates[0],
4649
+ managerCandidates: target.endpointCandidates,
4650
+ pairToken: target.pairToken,
4651
+ leaseId: target.leaseId,
4652
+ nodeId: target.nodeId,
4653
+ engine: 'auto',
4654
+ source: 'local-bridge-registry'
4655
+ });
4656
+
4657
+ const ok = connect?.ok === true;
4658
+ updateRemoteRegistryFollowerState({
4659
+ status: ok ? 'connected' : 'connect-failed',
4660
+ reason: ok ? (connect.alreadyRunning ? 'already-running' : 'connected') : 'connect-failed',
4661
+ authenticated: true,
4662
+ lastAttemptAt: attemptedAt,
4663
+ lastSuccessAt: ok ? new Date().toISOString() : remoteRegistryFollowerState.lastSuccessAt,
4664
+ targetEndpoint: target.endpointCandidates[0],
4665
+ targetEndpointCandidates: target.endpointCandidates,
4666
+ targetLeaseId: target.leaseId,
4667
+ targetNodeId: target.nodeId,
4668
+ lastError: ok ? '' : (connect?.error || 'remote-agent-connect-failed')
4669
+ });
4670
+ await reportRemoteRegistryFollowerSync({
4671
+ ok,
4672
+ attempted: true,
4673
+ connected: ok,
4674
+ skipped: connect?.alreadyRunning === true,
4675
+ reason: ok ? (connect.alreadyRunning ? 'already-running' : 'connected') : 'connect-failed',
4676
+ error: ok ? '' : connect?.error,
4677
+ trigger,
4678
+ authenticated: true,
4679
+ targetActive: true,
4680
+ targetEndpoint: target.endpointCandidates[0],
4681
+ targetEndpointCandidates: target.endpointCandidates,
4682
+ targetLeaseId: target.leaseId,
4683
+ targetNodeId: target.nodeId,
4684
+ targetExpiresAt: target.expiresAt
4685
+ });
4686
+
4687
+ scheduleRemoteRegistryFollower(ok ? REMOTE_REGISTRY_FOLLOWER_POLL_MS : REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS, ok ? 'connected' : 'connect-failed');
4688
+ return serializeRemoteRegistryFollowerState();
4689
+ } catch (error) {
4690
+ const transientAuth = error?.statusCode === 401 || error?.statusCode === 403;
4691
+ updateRemoteRegistryFollowerState({
4692
+ status: transientAuth ? 'auth-pending' : 'error',
4693
+ reason: transientAuth ? 'registry-auth-pending' : 'registry-sync-failed',
4694
+ authenticated: false,
4695
+ lastAttemptAt: attemptedAt,
4696
+ lastError: error?.message || String(error || '')
4697
+ });
4698
+ await reportRemoteRegistryFollowerSync({
4699
+ ok: isRemoteAgentProcessRunning(),
4700
+ skipped: true,
4701
+ reason: transientAuth ? 'registry-auth-pending-agent-kept' : 'registry-sync-failed',
4702
+ error: error?.message || String(error || ''),
4703
+ trigger,
4704
+ authenticated: false
4705
+ });
4706
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS, transientAuth ? 'auth-pending' : 'sync-error');
4707
+ return serializeRemoteRegistryFollowerState();
4708
+ } finally {
4709
+ remoteRegistryFollowerInFlight = false;
4710
+ }
4711
+ }
4712
+
4713
+ function startRemoteRegistryFollower() {
4714
+ if (!REMOTE_REGISTRY_FOLLOWER_ENABLED || remoteRegistryFollowerStarted) {
4715
+ return;
4716
+ }
4717
+
4718
+ remoteRegistryFollowerStarted = true;
4719
+ updateRemoteRegistryFollowerState({
4720
+ status: 'starting',
4721
+ reason: 'startup',
4722
+ lastAttemptAt: '',
4723
+ lastError: ''
4724
+ });
4725
+ scheduleRemoteRegistryFollower(250, 'startup');
4726
+ }
4727
+
4728
+ function stopRemoteRegistryFollower() {
4729
+ if (remoteRegistryFollowerTimer) {
4730
+ clearTimeout(remoteRegistryFollowerTimer);
4731
+ remoteRegistryFollowerTimer = null;
4732
+ }
4733
+ remoteRegistryFollowerStarted = false;
4734
+ updateRemoteRegistryFollowerState({
4735
+ status: REMOTE_REGISTRY_FOLLOWER_ENABLED ? 'stopped' : 'disabled',
4736
+ reason: 'bridge-shutdown'
4737
+ });
4738
+ }
4739
+
4157
4740
  function trimShellOutput(value, maxLength) {
4158
4741
  const text = String(value || '');
4159
4742
  if (text.length <= maxLength) {
4160
- return text;
4161
- }
4743
+ return text;
4744
+ }
4162
4745
 
4163
4746
  return text.slice(text.length - maxLength);
4164
4747
  }
@@ -8610,6 +9193,7 @@ app.get('/api/status', async (req, res) => {
8610
9193
  bridgeAuthRequired,
8611
9194
  remoteHub: remoteHub.getStatus({ includeSecrets: false }),
8612
9195
  remoteAgent: serializeRemoteAgentState(),
9196
+ remoteRegistryFollower: serializeRemoteRegistryFollowerState(),
8613
9197
  shellJobsPath: '/api/shell/jobs',
8614
9198
  companyCore: {
8615
9199
  baseUrl: companyCoreBaseUrl,
@@ -10400,15 +10984,14 @@ async function startBridgeServer() {
10400
10984
  `${tone('listen', 'muted')} ${tone(`http://127.0.0.1:${PORT}`, 'path')}`,
10401
10985
  `${tone('stop', 'muted')} Ctrl+C`
10402
10986
  ]);
10987
+ startRemoteRegistryFollower();
10403
10988
  });
10404
10989
  }
10405
10990
 
10406
10991
  await startBridgeServer();
10407
10992
 
10408
- // Graceful shutdown
10409
- let isShuttingDown = false;
10410
-
10411
- async function shutdownBridge(signal) {
10993
+ // Graceful shutdown
10994
+ async function shutdownBridge(signal) {
10412
10995
  if (isShuttingDown) return;
10413
10996
  isShuttingDown = true;
10414
10997
 
@@ -10435,6 +11018,12 @@ async function shutdownBridge(signal) {
10435
11018
  // Ignore if already closed
10436
11019
  }
10437
11020
 
11021
+ try {
11022
+ stopRemoteRegistryFollower();
11023
+ } catch {
11024
+ // Ignore registry follower shutdown errors
11025
+ }
11026
+
10438
11027
  try {
10439
11028
  await stopRemoteAgentConnection('bridge-shutdown');
10440
11029
  } catch {
@@ -13451,7 +13451,7 @@
13451
13451
  const REMOTE_FLEET_MONITOR_REFRESH_MS = 10000;
13452
13452
  const REMOTE_FLEET_EMPTY_MONITOR_REFRESH_MS = 2500;
13453
13453
  const REMOTE_FLEET_LIVE_REFRESH_MS = 30000;
13454
- const REMOTE_FLEET_MONITOR_LIVE_FPS = 10;
13454
+ const REMOTE_FLEET_MONITOR_LIVE_FPS = 12;
13455
13455
  const REMOTE_FLEET_LIVE_FRAME_REFRESH_MS = Math.round(1000 / REMOTE_FLEET_MONITOR_LIVE_FPS);
13456
13456
  const REMOTE_FLEET_CONTROL_LIVE_FPS = 20;
13457
13457
  const REMOTE_FLEET_CONTROL_FRAME_REFRESH_MS = Math.round(1000 / REMOTE_FLEET_CONTROL_LIVE_FPS);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "mainAssemblyName": "MindExecution.Web",
3
3
  "resources": {
4
- "hash": "sha256-aGxH2r2LOpixHAksGBYXRcA86Dv8rGHPIhoagHBUV2M=",
4
+ "hash": "sha256-exyWukcppwSmHQvoV122RgnUj4KZOvdS3LHrc3Z6BTY=",
5
5
  "fingerprinting": {
6
6
  "Google.Protobuf.9h59ukbel7.dll": "Google.Protobuf.dll",
7
7
  "Markdig.d1j7v41cl1.dll": "Markdig.dll",
@@ -127,12 +127,12 @@
127
127
  "MindExecution.Kernel.3zom09ir6j.dll": "MindExecution.Kernel.dll",
128
128
  "MindExecution.Plugins.Admin.bk23zzy6wc.dll": "MindExecution.Plugins.Admin.dll",
129
129
  "MindExecution.Plugins.Business.zwcvc8wufe.dll": "MindExecution.Plugins.Business.dll",
130
- "MindExecution.Plugins.Concept.lxunwcdjgq.dll": "MindExecution.Plugins.Concept.dll",
130
+ "MindExecution.Plugins.Concept.5gltgdqyo3.dll": "MindExecution.Plugins.Concept.dll",
131
131
  "MindExecution.Plugins.Directory.jc0g7fvyzv.dll": "MindExecution.Plugins.Directory.dll",
132
- "MindExecution.Plugins.PlanMaster.2sugb66h95.dll": "MindExecution.Plugins.PlanMaster.dll",
133
- "MindExecution.Plugins.YouTube.vy0nsbcoo4.dll": "MindExecution.Plugins.YouTube.dll",
134
- "MindExecution.Shared.vki6dm13ef.dll": "MindExecution.Shared.dll",
135
- "MindExecution.Web.994sb3nzuy.dll": "MindExecution.Web.dll",
132
+ "MindExecution.Plugins.PlanMaster.cn6ppe6qoz.dll": "MindExecution.Plugins.PlanMaster.dll",
133
+ "MindExecution.Plugins.YouTube.d7nnsxrftv.dll": "MindExecution.Plugins.YouTube.dll",
134
+ "MindExecution.Shared.mnpwk5h86c.dll": "MindExecution.Shared.dll",
135
+ "MindExecution.Web.ygql0sg6b3.dll": "MindExecution.Web.dll",
136
136
  "dotnet.js": "dotnet.js",
137
137
  "dotnet.native.qc8g39g30v.js": "dotnet.native.js",
138
138
  "dotnet.native.boem75ye5i.wasm": "dotnet.native.wasm",
@@ -280,16 +280,16 @@
280
280
  "netstandard.yvr3prsx0x.dll": "sha256-EksNn8Luo4bOWqJ6X7dIe9qG9oOqwOVzjH2xYyMNi+E=",
281
281
  "MindExecution.Core.asayb4hw1o.dll": "sha256-5gla4JTR/zbvkGpneSt6S7ItE51+MjWgjW6L+B6Yevk=",
282
282
  "MindExecution.Kernel.3zom09ir6j.dll": "sha256-78vR2SNkF9FwdQv0fEItSyz4HzgyzwGUROJAnpripPA=",
283
- "MindExecution.Plugins.Concept.lxunwcdjgq.dll": "sha256-WVyvjaxW6UoGQvGW0icChLsXwYVCcdn0BLTftzlkCKc=",
284
- "MindExecution.Plugins.PlanMaster.2sugb66h95.dll": "sha256-Lp0AieRHDo2H4Yl0Il1pPruy9nFUHJsX9t1AblXGD6Y=",
285
- "MindExecution.Shared.vki6dm13ef.dll": "sha256-cUcgQfOJa1LaZuLSXdrmi1RrrueXoKuP49M/cjsbguU=",
286
- "MindExecution.Web.994sb3nzuy.dll": "sha256-jQshtLmhEC4xenzdD9PqHjaCuKvJf/frzaKBi2weDAg="
283
+ "MindExecution.Plugins.Concept.5gltgdqyo3.dll": "sha256-1ZVpFoyPoidSyJz38jH40AFealONt+qQpToJKgqoWW8=",
284
+ "MindExecution.Plugins.PlanMaster.cn6ppe6qoz.dll": "sha256-GIFH/f4ilVTFDJVEDLLiWkkI7yGeeQl/T5/iuivI87Q=",
285
+ "MindExecution.Shared.mnpwk5h86c.dll": "sha256-Ht4Kl9nQePXwyDEwL4jBxo2K3F9R4B2OGnSuhHmZ+eU=",
286
+ "MindExecution.Web.ygql0sg6b3.dll": "sha256-vhb5mjLTaEStzJ9ggN4jBqtd1Z7FAkKHp7bx6Pz33Mc="
287
287
  },
288
288
  "lazyAssembly": {
289
289
  "MindExecution.Plugins.Admin.bk23zzy6wc.dll": "sha256-uz7Nz0OZ8BBESp2Nr9YocyzV0dUrn7szPuD4/m/RWL8=",
290
290
  "MindExecution.Plugins.Business.zwcvc8wufe.dll": "sha256-3YDsB2/uNI+feWqJk+cesLHgNt8s2/8hXxGPO6CRZZE=",
291
291
  "MindExecution.Plugins.Directory.jc0g7fvyzv.dll": "sha256-PSe7135JPpTBmu4j+rGLF0Dc/6w/wDJNPi8zlfTmSMk=",
292
- "MindExecution.Plugins.YouTube.vy0nsbcoo4.dll": "sha256-p8w1TI9Ri41EAG/j0MJCiuEcJdr4x/Hr5Spd2oluZjM="
292
+ "MindExecution.Plugins.YouTube.d7nnsxrftv.dll": "sha256-OQVjt1f240FGxRV8b2FpxDv4i3U4a7GGA+dBBiZBih8="
293
293
  }
294
294
  },
295
295
  "cacheBootResources": true,
@@ -7,8 +7,8 @@
7
7
  <title>MindExec | Run your ideas as AI task graphs</title>
8
8
  <meta name="description" content="MindExec is an AI execution canvas for solo builders, researchers, developers, and creators. Start with free browser tools, then move serious work into saved MindCanvas projects." />
9
9
  <base href="/" />
10
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260616-mdm-binary-canvas-v578" />
11
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260616-mdm-binary-canvas-v578" />
10
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260616-mdm-binary-canvas-v579" />
11
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260616-mdm-binary-canvas-v579" />
12
12
  <!-- ?쇄뼹??Font Awesome (local) ?쇄뼹??-->
13
13
  <link rel="stylesheet" href="_content/MindExecution.Shared/lib/font-awesome/css/all.min.css" />
14
14
  <!-- ?꿎뼯??-->
@@ -579,7 +579,7 @@
579
579
  }
580
580
 
581
581
  const base = '_content/MindExecution.Shared/js/';
582
- const scriptVersion = '20260616-mdm-binary-canvas-v578';
582
+ const scriptVersion = '20260616-mdm-binary-canvas-v579';
583
583
  const scriptUrl = (script) => `${base}${script}?v=${scriptVersion}`;
584
584
  console.log(`[Script Loader] Shared JS version: ${scriptVersion}`);
585
585
  const criticalScripts = [
@@ -1,5 +1,5 @@
1
1
  self.assetsManifest = {
2
- "version": "ssCNFqYX",
2
+ "version": "yUK/iR3X",
3
3
  "assets": [
4
4
  {
5
5
  "hash": "sha256-+CSYMcqLNTsq3VnH11jgYyOCCdxvHzL74CBmo4sCmMU=",
@@ -86,7 +86,7 @@
86
86
  "url": "_content/MindExecution.Shared/js/mind-map-core.js.backup"
87
87
  },
88
88
  {
89
- "hash": "sha256-cegQeRNPVyy9IFW9kOgLE0bhe4QOA0+Q72z3bheV0hc=",
89
+ "hash": "sha256-RLe5Ta1um44TU9QYuOpzKHrl8t1tOWQdkL+zwH79MFA=",
90
90
  "url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
91
91
  },
92
92
  {
@@ -426,28 +426,28 @@
426
426
  "url": "_framework/MindExecution.Plugins.Business.zwcvc8wufe.dll"
427
427
  },
428
428
  {
429
- "hash": "sha256-WVyvjaxW6UoGQvGW0icChLsXwYVCcdn0BLTftzlkCKc=",
430
- "url": "_framework/MindExecution.Plugins.Concept.lxunwcdjgq.dll"
429
+ "hash": "sha256-1ZVpFoyPoidSyJz38jH40AFealONt+qQpToJKgqoWW8=",
430
+ "url": "_framework/MindExecution.Plugins.Concept.5gltgdqyo3.dll"
431
431
  },
432
432
  {
433
433
  "hash": "sha256-PSe7135JPpTBmu4j+rGLF0Dc/6w/wDJNPi8zlfTmSMk=",
434
434
  "url": "_framework/MindExecution.Plugins.Directory.jc0g7fvyzv.dll"
435
435
  },
436
436
  {
437
- "hash": "sha256-Lp0AieRHDo2H4Yl0Il1pPruy9nFUHJsX9t1AblXGD6Y=",
438
- "url": "_framework/MindExecution.Plugins.PlanMaster.2sugb66h95.dll"
437
+ "hash": "sha256-GIFH/f4ilVTFDJVEDLLiWkkI7yGeeQl/T5/iuivI87Q=",
438
+ "url": "_framework/MindExecution.Plugins.PlanMaster.cn6ppe6qoz.dll"
439
439
  },
440
440
  {
441
- "hash": "sha256-p8w1TI9Ri41EAG/j0MJCiuEcJdr4x/Hr5Spd2oluZjM=",
442
- "url": "_framework/MindExecution.Plugins.YouTube.vy0nsbcoo4.dll"
441
+ "hash": "sha256-OQVjt1f240FGxRV8b2FpxDv4i3U4a7GGA+dBBiZBih8=",
442
+ "url": "_framework/MindExecution.Plugins.YouTube.d7nnsxrftv.dll"
443
443
  },
444
444
  {
445
- "hash": "sha256-cUcgQfOJa1LaZuLSXdrmi1RrrueXoKuP49M/cjsbguU=",
446
- "url": "_framework/MindExecution.Shared.vki6dm13ef.dll"
445
+ "hash": "sha256-Ht4Kl9nQePXwyDEwL4jBxo2K3F9R4B2OGnSuhHmZ+eU=",
446
+ "url": "_framework/MindExecution.Shared.mnpwk5h86c.dll"
447
447
  },
448
448
  {
449
- "hash": "sha256-jQshtLmhEC4xenzdD9PqHjaCuKvJf/frzaKBi2weDAg=",
450
- "url": "_framework/MindExecution.Web.994sb3nzuy.dll"
449
+ "hash": "sha256-vhb5mjLTaEStzJ9ggN4jBqtd1Z7FAkKHp7bx6Pz33Mc=",
450
+ "url": "_framework/MindExecution.Web.ygql0sg6b3.dll"
451
451
  },
452
452
  {
453
453
  "hash": "sha256-IsZJ91/OW+fHzNqIgEc7Y072ns8z9dGritiSyvR9Wgc=",
@@ -770,7 +770,7 @@
770
770
  "url": "_framework/Websocket.Client.vapounvmnl.dll"
771
771
  },
772
772
  {
773
- "hash": "sha256-RIddmeeu4/GO5kDBZ6kmWUv1o1+x8NzfFxWruTBtPec=",
773
+ "hash": "sha256-FgsKNE/RFoMI9OioRZ4cgImxpXXkbc2D4LpHp7W2L7A=",
774
774
  "url": "_framework/blazor.boot.json"
775
775
  },
776
776
  {
@@ -834,7 +834,7 @@
834
834
  "url": "image-manifest.json"
835
835
  },
836
836
  {
837
- "hash": "sha256-xRw/+Q0xTn1kYsE6nXsqd3DUoiZOzocvU7tivvgtQfQ=",
837
+ "hash": "sha256-0O54YE8cYfnYOu/2NbpFuJs/cJRKBw/OAc963M3JI6E=",
838
838
  "url": "index.html"
839
839
  },
840
840
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: ssCNFqYX */
1
+ /* Manifest version: yUK/iR3X */
2
2
  // Hosted deployments should prefer the network over stale offline caches.
3
3
  // This service worker immediately clears old Blazor offline caches and unregisters itself.
4
4