@mindexec/cli 0.2.133 → 0.2.135

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/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
  });
@@ -2124,6 +2128,11 @@ const remoteFrameWss = new WebSocketServer({ noServer: true });
2124
2128
  const wsClients = new Set();
2125
2129
  const remoteFrameClients = new Set();
2126
2130
  const shellJobs = new Map();
2131
+ const REMOTE_FRAME_WS_AUTO_START_LIMIT = 120;
2132
+ const REMOTE_FRAME_WS_DEFAULT_FPS = 12;
2133
+ const REMOTE_FRAME_WS_DEFAULT_MAX_WIDTH = 960;
2134
+ const REMOTE_FRAME_WS_DEFAULT_MAX_HEIGHT = 540;
2135
+ const REMOTE_FRAME_WS_DEFAULT_QUALITY = 60;
2127
2136
 
2128
2137
  httpServer.on('upgrade', (req, socket, head) => {
2129
2138
  try {
@@ -2213,12 +2222,101 @@ function updateRemoteFrameClientSubscription(ws, payload = {}) {
2213
2222
  const deviceIds = normalizeRemoteFrameWsDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId);
2214
2223
  ws.remoteFrameDeviceIds = new Set(deviceIds);
2215
2224
  ws.remoteFrameSubscriptionUpdatedAt = new Date().toISOString();
2225
+ ws.remoteFrameAutoStartLive = /^(1|true|yes|on|live)$/i.test(String(payload.autoStartLive ?? payload.startLive ?? ''));
2226
+ ws.remoteFrameAutoStartOptions = normalizeRemoteFrameWsLiveOptions(payload);
2216
2227
  sendRemoteFrameClientJson(ws, {
2217
2228
  type: 'RemoteFrameSubscription',
2218
2229
  timestamp: ws.remoteFrameSubscriptionUpdatedAt,
2219
2230
  deviceIds,
2220
- mode: deviceIds.length > 0 ? 'selected-devices' : 'all-devices'
2231
+ mode: deviceIds.length > 0 ? 'selected-devices' : 'all-devices',
2232
+ autoStartLive: ws.remoteFrameAutoStartLive === true
2221
2233
  });
2234
+ maybeAutoStartRemoteFrameLiveStreams(ws, deviceIds);
2235
+ }
2236
+
2237
+ function clampRemoteFrameWsNumber(value, min, max, fallback) {
2238
+ const number = Number(value);
2239
+ if (!Number.isFinite(number)) {
2240
+ return fallback;
2241
+ }
2242
+
2243
+ return Math.max(min, Math.min(max, Math.round(number)));
2244
+ }
2245
+
2246
+ function normalizeRemoteFrameWsLiveOptions(payload = {}) {
2247
+ return {
2248
+ fps: clampRemoteFrameWsNumber(payload.fps, 1, 24, REMOTE_FRAME_WS_DEFAULT_FPS),
2249
+ maxWidth: clampRemoteFrameWsNumber(payload.maxWidth, 320, 2560, REMOTE_FRAME_WS_DEFAULT_MAX_WIDTH),
2250
+ maxHeight: clampRemoteFrameWsNumber(payload.maxHeight, 180, 1440, REMOTE_FRAME_WS_DEFAULT_MAX_HEIGHT),
2251
+ quality: clampRemoteFrameWsNumber(payload.quality, 20, 95, REMOTE_FRAME_WS_DEFAULT_QUALITY),
2252
+ mode: String(payload.mode || 'remote-fast').trim() || 'remote-fast'
2253
+ };
2254
+ }
2255
+
2256
+ function getRemoteFrameWsDeviceLookup() {
2257
+ const devices = remoteHub.listDevices({ includeDataUrl: false });
2258
+ return new Map(devices.map(device => [String(device?.deviceId || '').trim(), device]).filter(([id]) => id));
2259
+ }
2260
+
2261
+ function maybeAutoStartRemoteFrameLiveStreams(ws, deviceIds = []) {
2262
+ if (!ws || ws.remoteFrameAutoStartLive !== true || !Array.isArray(deviceIds) || deviceIds.length === 0) {
2263
+ return;
2264
+ }
2265
+
2266
+ const options = ws.remoteFrameAutoStartOptions || normalizeRemoteFrameWsLiveOptions();
2267
+ const lookup = getRemoteFrameWsDeviceLookup();
2268
+ const started = [];
2269
+ const skipped = [];
2270
+ for (const deviceId of deviceIds.slice(0, REMOTE_FRAME_WS_AUTO_START_LIMIT)) {
2271
+ const device = lookup.get(String(deviceId || '').trim());
2272
+ if (!device?.connected) {
2273
+ skipped.push({ deviceId, reason: 'not-connected' });
2274
+ continue;
2275
+ }
2276
+
2277
+ if (device.liveStreamActive === true) {
2278
+ skipped.push({ deviceId, reason: 'already-live' });
2279
+ continue;
2280
+ }
2281
+
2282
+ const liveCapable = device.liveStreamEnabled === true
2283
+ || device.liveStreamCapable === true
2284
+ || device.capabilities?.liveStream === true
2285
+ || device.capabilities?.stream === true
2286
+ || device.capabilities?.screenStream === true;
2287
+ if (!liveCapable) {
2288
+ skipped.push({ deviceId, reason: 'live-unavailable' });
2289
+ continue;
2290
+ }
2291
+
2292
+ const result = remoteHub.startLiveStream(deviceId, {
2293
+ ...options,
2294
+ streamId: `mdm-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
2295
+ });
2296
+ if (result?.ok === true) {
2297
+ started.push({
2298
+ deviceId,
2299
+ streamId: result.streamId || '',
2300
+ fps: result.fps || options.fps
2301
+ });
2302
+ } else {
2303
+ skipped.push({
2304
+ deviceId,
2305
+ reason: result?.error || 'start-failed'
2306
+ });
2307
+ }
2308
+ }
2309
+
2310
+ if (started.length > 0 || skipped.length > 0) {
2311
+ sendRemoteFrameClientJson(ws, {
2312
+ type: 'RemoteFrameLiveAutoStart',
2313
+ timestamp: new Date().toISOString(),
2314
+ requested: Math.min(deviceIds.length, REMOTE_FRAME_WS_AUTO_START_LIMIT),
2315
+ started,
2316
+ skipped: skipped.slice(0, 24),
2317
+ options
2318
+ });
2319
+ }
2222
2320
  }
2223
2321
 
2224
2322
  function buildRemoteFrameBinaryPacket(frameEvent) {
@@ -2912,6 +3010,10 @@ const REMOTE_AGENT_MAX_PARALLEL_CANDIDATES = 6;
2912
3010
  const REMOTE_AGENT_RECENT_MANAGER_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
2913
3011
  const REMOTE_AGENT_RECENT_MANAGER_CACHE_LIMIT = 64;
2914
3012
  const REMOTE_AGENT_RECENT_MANAGER_DEFAULT_KEY = 'default';
3013
+ const REMOTE_REGISTRY_FOLLOWER_ENABLED = !/^(0|false|no|off)$/i.test(String(process.env.MINDEXEC_REMOTE_REGISTRY_FOLLOWER || 'true'));
3014
+ const REMOTE_REGISTRY_FOLLOWER_POLL_MS = Math.max(1500, Number(process.env.MINDEXEC_REMOTE_REGISTRY_POLL_MS || 5000) || 5000);
3015
+ const REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS = Math.max(500, Number(process.env.MINDEXEC_REMOTE_REGISTRY_FAST_RETRY_MS || 1200) || 1200);
3016
+ const REMOTE_REGISTRY_FOLLOWER_MISSING_SESSION_STOP_COUNT = 3;
2915
3017
  let remoteAgentState = createRemoteAgentIdleState();
2916
3018
  let remoteAgentSyncReportState = null;
2917
3019
  let remoteAgentSyncReportLogKey = '';
@@ -2920,6 +3022,23 @@ let remoteAgentConnectPromise = null;
2920
3022
  const remoteAgentRecentSuccessfulManagers = new Map();
2921
3023
  let remoteAgentRecentSuccessfulManagersLoaded = false;
2922
3024
  let remoteAgentRecentSuccessfulManagersSavePromise = null;
3025
+ let remoteRegistryFollowerTimer = null;
3026
+ let remoteRegistryFollowerInFlight = false;
3027
+ let remoteRegistryFollowerStarted = false;
3028
+ let remoteRegistryFollowerConsecutiveMissingSession = 0;
3029
+ let remoteRegistryFollowerState = {
3030
+ enabled: REMOTE_REGISTRY_FOLLOWER_ENABLED,
3031
+ status: REMOTE_REGISTRY_FOLLOWER_ENABLED ? 'idle' : 'disabled',
3032
+ reason: '',
3033
+ lastAttemptAt: '',
3034
+ lastSuccessAt: '',
3035
+ lastError: '',
3036
+ targetEndpoint: '',
3037
+ targetEndpointCandidates: [],
3038
+ targetLeaseId: '',
3039
+ targetNodeId: '',
3040
+ authenticated: false
3041
+ };
2923
3042
 
2924
3043
  function createRemoteAgentIdleState(overrides = {}) {
2925
3044
  return {
@@ -4154,11 +4273,569 @@ async function startRemoteAgentConnectionAttempt(options = {}) {
4154
4273
  return { ok: true, alreadyRunning: false, agent: serializeRemoteAgentState() };
4155
4274
  }
4156
4275
 
4276
+ function serializeRemoteRegistryFollowerState() {
4277
+ return {
4278
+ ...remoteRegistryFollowerState,
4279
+ pollMs: REMOTE_REGISTRY_FOLLOWER_POLL_MS,
4280
+ fastRetryMs: REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS
4281
+ };
4282
+ }
4283
+
4284
+ function updateRemoteRegistryFollowerState(patch = {}) {
4285
+ remoteRegistryFollowerState = {
4286
+ ...remoteRegistryFollowerState,
4287
+ ...patch,
4288
+ enabled: REMOTE_REGISTRY_FOLLOWER_ENABLED
4289
+ };
4290
+ emitBridgeEvent('RemoteRegistryFollowerUpdated', serializeRemoteRegistryFollowerState());
4291
+ }
4292
+
4293
+ function readSupabaseRuntimeConfig() {
4294
+ const envUrl = String(process.env.SUPABASE_URL || process.env.MINDEXEC_SUPABASE_URL || '').trim();
4295
+ const envKey = String(
4296
+ process.env.SUPABASE_KEY
4297
+ || process.env.SUPABASE_ANON_KEY
4298
+ || process.env.SUPABASE_PUBLISHABLE_KEY
4299
+ || process.env.MINDEXEC_SUPABASE_KEY
4300
+ || '').trim();
4301
+ if (envUrl && envKey) {
4302
+ return { url: envUrl.replace(/\/+$/, ''), key: envKey };
4303
+ }
4304
+
4305
+ const candidates = [
4306
+ path.join(WEB_APP_ROOT || '', 'appsettings.json'),
4307
+ path.join(DEFAULT_WEB_APP_ROOT, 'appsettings.json'),
4308
+ path.resolve(BRIDGE_ROOT, '..', 'MindExecution.Web', 'wwwroot', 'appsettings.json')
4309
+ ].filter(Boolean);
4310
+
4311
+ for (const candidate of Array.from(new Set(candidates.map(item => path.resolve(item))))) {
4312
+ try {
4313
+ const payload = JSON.parse(readFileSync(candidate, 'utf8'));
4314
+ const url = String(payload?.Supabase?.Url || '').trim().replace(/\/+$/, '');
4315
+ const key = String(payload?.Supabase?.Key || payload?.Supabase?.AnonKey || payload?.Supabase?.PublishableKey || '').trim();
4316
+ if (url && key) {
4317
+ return { url, key };
4318
+ }
4319
+ } catch {
4320
+ // Try the next appsettings candidate.
4321
+ }
4322
+ }
4323
+
4324
+ return { url: '', key: '' };
4325
+ }
4326
+
4327
+ function readSupabaseSessionField(source, ...keys) {
4328
+ for (const key of keys) {
4329
+ const value = source?.[key];
4330
+ if (value !== undefined && value !== null && String(value).trim()) {
4331
+ return value;
4332
+ }
4333
+ }
4334
+ return '';
4335
+ }
4336
+
4337
+ function parseSupabaseSessionForRegistry(content) {
4338
+ let session = null;
4339
+ try {
4340
+ session = JSON.parse(String(content || ''));
4341
+ } catch {
4342
+ return null;
4343
+ }
4344
+
4345
+ const user = session?.user || session?.User || {};
4346
+ const accessToken = String(readSupabaseSessionField(session, 'access_token', 'accessToken', 'AccessToken')).trim();
4347
+ const userId = String(
4348
+ readSupabaseSessionField(user, 'id', 'Id')
4349
+ || readSupabaseSessionField(session, 'user_id', 'userId', 'UserId')).trim();
4350
+ const expiresAtRaw = Number(readSupabaseSessionField(session, 'expires_at', 'expiresAt', 'ExpiresAt') || 0);
4351
+ const expiresAtMs = Number.isFinite(expiresAtRaw) && expiresAtRaw > 0
4352
+ ? (expiresAtRaw > 9999999999 ? expiresAtRaw : expiresAtRaw * 1000)
4353
+ : 0;
4354
+
4355
+ if (!accessToken || !userId) {
4356
+ return null;
4357
+ }
4358
+
4359
+ return {
4360
+ accessToken,
4361
+ userId,
4362
+ expiresAtMs
4363
+ };
4364
+ }
4365
+
4366
+ function readRegistryTargetField(target, ...keys) {
4367
+ for (const key of keys) {
4368
+ const value = target?.[key];
4369
+ if (value !== undefined && value !== null) {
4370
+ return value;
4371
+ }
4372
+ }
4373
+ return '';
4374
+ }
4375
+
4376
+ function parseRegistryEndpointCandidates(value) {
4377
+ if (Array.isArray(value)) {
4378
+ return value.map(item => String(item || '').trim()).filter(Boolean);
4379
+ }
4380
+
4381
+ const text = String(value || '').trim();
4382
+ if (!text) {
4383
+ return [];
4384
+ }
4385
+
4386
+ try {
4387
+ const parsed = JSON.parse(text);
4388
+ if (Array.isArray(parsed)) {
4389
+ return parsed.map(item => String(item || '').trim()).filter(Boolean);
4390
+ }
4391
+ } catch {
4392
+ // Fall through to comma/newline split.
4393
+ }
4394
+
4395
+ return text.split(/[,\n]/g).map(item => item.trim()).filter(Boolean);
4396
+ }
4397
+
4398
+ function normalizeRemoteRegistryTarget(row) {
4399
+ if (!row || typeof row !== 'object') {
4400
+ return null;
4401
+ }
4402
+
4403
+ const activeRaw = readRegistryTargetField(row, 'active', 'Active');
4404
+ const active = activeRaw === true || /^(1|true|yes|on)$/i.test(String(activeRaw || ''));
4405
+ const endpoint = String(readRegistryTargetField(row, 'endpoint', 'Endpoint') || '').trim();
4406
+ const endpointCandidates = normalizeRemoteManagerEndpointList(
4407
+ parseRegistryEndpointCandidates(readRegistryTargetField(row, 'endpoint_candidates', 'endpointCandidates', 'EndpointCandidates')),
4408
+ endpoint);
4409
+ const pairToken = String(readRegistryTargetField(row, 'pair_token', 'pairToken', 'PairToken') || '').trim();
4410
+ const leaseId = String(readRegistryTargetField(row, 'lease_id', 'leaseId', 'LeaseId') || '').trim();
4411
+ const nodeId = String(readRegistryTargetField(row, 'node_id', 'nodeId', 'NodeId') || '').trim();
4412
+ const hostInstanceId = String(readRegistryTargetField(row, 'host_instance_id', 'hostInstanceId', 'HostInstanceId') || '').trim();
4413
+ const expiresAt = String(readRegistryTargetField(row, 'expires_at', 'expiresAt', 'ExpiresAt') || '').trim();
4414
+ const expiresAtMs = Date.parse(expiresAt);
4415
+
4416
+ return {
4417
+ active,
4418
+ endpoint,
4419
+ endpointCandidates,
4420
+ pairToken,
4421
+ leaseId,
4422
+ nodeId,
4423
+ hostInstanceId,
4424
+ expiresAt,
4425
+ expiresAtMs: Number.isFinite(expiresAtMs) ? expiresAtMs : 0
4426
+ };
4427
+ }
4428
+
4429
+ function isRemoteRegistryTargetExpired(target) {
4430
+ return !target?.expiresAtMs || target.expiresAtMs <= Date.now();
4431
+ }
4432
+
4433
+ function isRemoteRegistryTargetSameAsLocalHost(localHub, target) {
4434
+ const localLeaseId = safeRemoteAgentField(localHub?.hostTargetLeaseId, 128);
4435
+ const localHostInstanceId = safeRemoteAgentField(localHub?.hostTargetHostInstanceId || localHub?.hostInstanceId, 128);
4436
+ return !!localLeaseId
4437
+ && !!localHostInstanceId
4438
+ && !!target?.leaseId
4439
+ && !!target?.hostInstanceId
4440
+ && localLeaseId.toLowerCase() === String(target.leaseId).toLowerCase()
4441
+ && localHostInstanceId.toLowerCase() === String(target.hostInstanceId).toLowerCase();
4442
+ }
4443
+
4444
+ async function fetchRemoteRegistryTarget(config, session) {
4445
+ const url = new URL('/rest/v1/remote_host_targets', config.url);
4446
+ url.searchParams.set('select', '*');
4447
+ url.searchParams.set('user_id', `eq.${session.userId}`);
4448
+ url.searchParams.set('limit', '1');
4449
+
4450
+ const response = await fetch(url.toString(), {
4451
+ method: 'GET',
4452
+ headers: {
4453
+ apikey: config.key,
4454
+ Authorization: `Bearer ${session.accessToken}`,
4455
+ Accept: 'application/json'
4456
+ }
4457
+ });
4458
+
4459
+ if (!response.ok) {
4460
+ const text = await response.text().catch(() => '');
4461
+ const error = new Error(`registry-rest-${response.status}${text ? `:${shortenText(text, 160)}` : ''}`);
4462
+ error.statusCode = response.status;
4463
+ throw error;
4464
+ }
4465
+
4466
+ const payload = await response.json();
4467
+ return normalizeRemoteRegistryTarget(Array.isArray(payload) ? payload[0] : payload);
4468
+ }
4469
+
4470
+ async function reportRemoteRegistryFollowerSync(payload = {}) {
4471
+ const report = createRemoteAgentSyncReport(payload);
4472
+ rememberRemoteAgentSyncReport(report);
4473
+ return report;
4474
+ }
4475
+
4476
+ function scheduleRemoteRegistryFollower(delayMs = REMOTE_REGISTRY_FOLLOWER_POLL_MS, reason = 'timer') {
4477
+ if (!REMOTE_REGISTRY_FOLLOWER_ENABLED || isShuttingDown) {
4478
+ return;
4479
+ }
4480
+
4481
+ if (remoteRegistryFollowerTimer) {
4482
+ clearTimeout(remoteRegistryFollowerTimer);
4483
+ remoteRegistryFollowerTimer = null;
4484
+ }
4485
+
4486
+ remoteRegistryFollowerTimer = setTimeout(() => {
4487
+ remoteRegistryFollowerTimer = null;
4488
+ runRemoteRegistryFollowerOnce(reason).catch(error => {
4489
+ logWarn('remote', `registry follower failed: ${error?.message || error}`);
4490
+ updateRemoteRegistryFollowerState({
4491
+ status: 'error',
4492
+ reason: 'follower-error',
4493
+ lastError: error?.message || String(error || ''),
4494
+ lastAttemptAt: new Date().toISOString()
4495
+ });
4496
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS, 'error-retry');
4497
+ });
4498
+ }, Math.max(0, Number(delayMs) || 0));
4499
+ remoteRegistryFollowerTimer?.unref?.();
4500
+ }
4501
+
4502
+ async function wakeRemoteRegistryFollower(reason = 'wake') {
4503
+ if (!REMOTE_REGISTRY_FOLLOWER_ENABLED) {
4504
+ return;
4505
+ }
4506
+ scheduleRemoteRegistryFollower(0, reason);
4507
+ }
4508
+
4509
+ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
4510
+ if (!REMOTE_REGISTRY_FOLLOWER_ENABLED || remoteRegistryFollowerInFlight) {
4511
+ return serializeRemoteRegistryFollowerState();
4512
+ }
4513
+
4514
+ remoteRegistryFollowerInFlight = true;
4515
+ const attemptedAt = new Date().toISOString();
4516
+ try {
4517
+ const config = readSupabaseRuntimeConfig();
4518
+ if (!config.url || !config.key) {
4519
+ updateRemoteRegistryFollowerState({
4520
+ status: 'skipped',
4521
+ reason: 'supabase-config-missing',
4522
+ authenticated: false,
4523
+ lastAttemptAt: attemptedAt,
4524
+ lastError: ''
4525
+ });
4526
+ await reportRemoteRegistryFollowerSync({
4527
+ ok: false,
4528
+ skipped: true,
4529
+ reason: 'supabase-config-missing',
4530
+ trigger,
4531
+ authenticated: false
4532
+ });
4533
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'config-missing');
4534
+ return serializeRemoteRegistryFollowerState();
4535
+ }
4536
+
4537
+ const sessionPayload = await readStableAuthSessionPayload();
4538
+ const session = sessionPayload.found ? parseSupabaseSessionForRegistry(sessionPayload.content) : null;
4539
+ if (!session) {
4540
+ remoteRegistryFollowerConsecutiveMissingSession += 1;
4541
+ if (remoteRegistryFollowerConsecutiveMissingSession >= REMOTE_REGISTRY_FOLLOWER_MISSING_SESSION_STOP_COUNT
4542
+ && isRemoteAgentProcessRunning()) {
4543
+ await stopRemoteAgentConnection('registry-not-authenticated');
4544
+ }
4545
+
4546
+ updateRemoteRegistryFollowerState({
4547
+ status: 'skipped',
4548
+ reason: 'registry-not-authenticated',
4549
+ authenticated: false,
4550
+ lastAttemptAt: attemptedAt,
4551
+ lastError: '',
4552
+ targetEndpoint: '',
4553
+ targetEndpointCandidates: [],
4554
+ targetLeaseId: '',
4555
+ targetNodeId: ''
4556
+ });
4557
+ await reportRemoteRegistryFollowerSync({
4558
+ ok: true,
4559
+ skipped: true,
4560
+ reason: 'registry-not-authenticated',
4561
+ trigger,
4562
+ authenticated: false
4563
+ });
4564
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'auth-missing');
4565
+ return serializeRemoteRegistryFollowerState();
4566
+ }
4567
+
4568
+ remoteRegistryFollowerConsecutiveMissingSession = 0;
4569
+ if (session.expiresAtMs && session.expiresAtMs <= Date.now()) {
4570
+ updateRemoteRegistryFollowerState({
4571
+ status: 'skipped',
4572
+ reason: 'session-expired',
4573
+ authenticated: false,
4574
+ lastAttemptAt: attemptedAt,
4575
+ lastError: ''
4576
+ });
4577
+ await reportRemoteRegistryFollowerSync({
4578
+ ok: true,
4579
+ skipped: true,
4580
+ reason: 'session-expired',
4581
+ trigger,
4582
+ authenticated: false
4583
+ });
4584
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'session-expired');
4585
+ return serializeRemoteRegistryFollowerState();
4586
+ }
4587
+
4588
+ const localHub = remoteHub.getStatus({ includeSecrets: false });
4589
+ const target = await fetchRemoteRegistryTarget(config, session);
4590
+
4591
+ if (localHub?.hostTargetActive === true) {
4592
+ if (target?.active === true
4593
+ && !isRemoteRegistryTargetExpired(target)
4594
+ && !isRemoteRegistryTargetSameAsLocalHost(localHub, target)) {
4595
+ remoteHub.setHostTarget({
4596
+ enabled: false,
4597
+ nodeId: localHub.hostTargetNodeId
4598
+ });
4599
+ updateRemoteRegistryFollowerState({
4600
+ status: 'local-host-superseded',
4601
+ reason: 'local-host-superseded',
4602
+ authenticated: true,
4603
+ lastAttemptAt: attemptedAt,
4604
+ lastSuccessAt: attemptedAt,
4605
+ targetEndpoint: target.endpoint,
4606
+ targetEndpointCandidates: target.endpointCandidates,
4607
+ targetLeaseId: target.leaseId,
4608
+ targetNodeId: target.nodeId,
4609
+ lastError: ''
4610
+ });
4611
+ await reportRemoteRegistryFollowerSync({
4612
+ ok: true,
4613
+ attempted: true,
4614
+ disconnected: true,
4615
+ reason: 'local-host-superseded',
4616
+ trigger,
4617
+ authenticated: true,
4618
+ targetActive: true,
4619
+ targetEndpoint: target.endpoint,
4620
+ targetEndpointCandidates: target.endpointCandidates,
4621
+ targetLeaseId: target.leaseId,
4622
+ targetNodeId: target.nodeId,
4623
+ targetExpiresAt: target.expiresAt
4624
+ });
4625
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS, 'local-host-cleared');
4626
+ return serializeRemoteRegistryFollowerState();
4627
+ }
4628
+
4629
+ if (isRemoteAgentProcessRunning()) {
4630
+ await stopRemoteAgentConnection('local-host-target-active');
4631
+ }
4632
+ updateRemoteRegistryFollowerState({
4633
+ status: 'skipped',
4634
+ reason: 'local-monitor-is-host',
4635
+ authenticated: true,
4636
+ lastAttemptAt: attemptedAt,
4637
+ lastSuccessAt: attemptedAt,
4638
+ lastError: ''
4639
+ });
4640
+ await reportRemoteRegistryFollowerSync({
4641
+ ok: true,
4642
+ skipped: true,
4643
+ reason: 'local-monitor-is-host',
4644
+ trigger,
4645
+ authenticated: true,
4646
+ localHostTargetActive: true,
4647
+ localHostTargetLeaseId: localHub.hostTargetLeaseId,
4648
+ localHostTargetNodeId: localHub.hostTargetNodeId
4649
+ });
4650
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'local-host');
4651
+ return serializeRemoteRegistryFollowerState();
4652
+ }
4653
+
4654
+ if (!target?.active || isRemoteRegistryTargetExpired(target)) {
4655
+ if (isRemoteAgentProcessRunning()) {
4656
+ await stopRemoteAgentConnection('registry-inactive');
4657
+ }
4658
+ updateRemoteRegistryFollowerState({
4659
+ status: 'idle',
4660
+ reason: 'no-active-target',
4661
+ authenticated: true,
4662
+ lastAttemptAt: attemptedAt,
4663
+ lastSuccessAt: attemptedAt,
4664
+ targetEndpoint: '',
4665
+ targetEndpointCandidates: [],
4666
+ targetLeaseId: '',
4667
+ targetNodeId: '',
4668
+ lastError: ''
4669
+ });
4670
+ await reportRemoteRegistryFollowerSync({
4671
+ ok: true,
4672
+ skipped: true,
4673
+ reason: 'no-active-target',
4674
+ trigger,
4675
+ authenticated: true
4676
+ });
4677
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'no-target');
4678
+ return serializeRemoteRegistryFollowerState();
4679
+ }
4680
+
4681
+ if (isRemoteRegistryTargetSameAsLocalHost(localHub, target)) {
4682
+ updateRemoteRegistryFollowerState({
4683
+ status: 'skipped',
4684
+ reason: 'same-host-lease',
4685
+ authenticated: true,
4686
+ lastAttemptAt: attemptedAt,
4687
+ lastSuccessAt: attemptedAt,
4688
+ targetEndpoint: target.endpoint,
4689
+ targetEndpointCandidates: target.endpointCandidates,
4690
+ targetLeaseId: target.leaseId,
4691
+ targetNodeId: target.nodeId,
4692
+ lastError: ''
4693
+ });
4694
+ await reportRemoteRegistryFollowerSync({
4695
+ ok: true,
4696
+ skipped: true,
4697
+ reason: 'same-host-lease',
4698
+ trigger,
4699
+ authenticated: true,
4700
+ targetActive: true,
4701
+ targetEndpoint: target.endpoint,
4702
+ targetEndpointCandidates: target.endpointCandidates,
4703
+ targetLeaseId: target.leaseId,
4704
+ targetNodeId: target.nodeId,
4705
+ targetExpiresAt: target.expiresAt
4706
+ });
4707
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'same-host');
4708
+ return serializeRemoteRegistryFollowerState();
4709
+ }
4710
+
4711
+ if (target.endpointCandidates.length === 0 || !target.pairToken) {
4712
+ updateRemoteRegistryFollowerState({
4713
+ status: 'skipped',
4714
+ reason: 'target-missing-endpoint-or-pair',
4715
+ authenticated: true,
4716
+ lastAttemptAt: attemptedAt,
4717
+ lastSuccessAt: attemptedAt,
4718
+ targetEndpoint: target.endpoint,
4719
+ targetEndpointCandidates: target.endpointCandidates,
4720
+ targetLeaseId: target.leaseId,
4721
+ targetNodeId: target.nodeId,
4722
+ lastError: ''
4723
+ });
4724
+ await reportRemoteRegistryFollowerSync({
4725
+ ok: true,
4726
+ skipped: true,
4727
+ reason: 'target-missing-endpoint-or-pair',
4728
+ trigger,
4729
+ authenticated: true,
4730
+ targetActive: true,
4731
+ targetEndpoint: target.endpoint,
4732
+ targetEndpointCandidates: target.endpointCandidates,
4733
+ targetLeaseId: target.leaseId,
4734
+ targetNodeId: target.nodeId,
4735
+ targetExpiresAt: target.expiresAt
4736
+ });
4737
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'target-incomplete');
4738
+ return serializeRemoteRegistryFollowerState();
4739
+ }
4740
+
4741
+ const connect = await startRemoteAgentConnection({
4742
+ manager: target.endpointCandidates[0],
4743
+ managerCandidates: target.endpointCandidates,
4744
+ pairToken: target.pairToken,
4745
+ leaseId: target.leaseId,
4746
+ nodeId: target.nodeId,
4747
+ engine: 'auto',
4748
+ source: 'local-bridge-registry'
4749
+ });
4750
+
4751
+ const ok = connect?.ok === true;
4752
+ updateRemoteRegistryFollowerState({
4753
+ status: ok ? 'connected' : 'connect-failed',
4754
+ reason: ok ? (connect.alreadyRunning ? 'already-running' : 'connected') : 'connect-failed',
4755
+ authenticated: true,
4756
+ lastAttemptAt: attemptedAt,
4757
+ lastSuccessAt: ok ? new Date().toISOString() : remoteRegistryFollowerState.lastSuccessAt,
4758
+ targetEndpoint: target.endpointCandidates[0],
4759
+ targetEndpointCandidates: target.endpointCandidates,
4760
+ targetLeaseId: target.leaseId,
4761
+ targetNodeId: target.nodeId,
4762
+ lastError: ok ? '' : (connect?.error || 'remote-agent-connect-failed')
4763
+ });
4764
+ await reportRemoteRegistryFollowerSync({
4765
+ ok,
4766
+ attempted: true,
4767
+ connected: ok,
4768
+ skipped: connect?.alreadyRunning === true,
4769
+ reason: ok ? (connect.alreadyRunning ? 'already-running' : 'connected') : 'connect-failed',
4770
+ error: ok ? '' : connect?.error,
4771
+ trigger,
4772
+ authenticated: true,
4773
+ targetActive: true,
4774
+ targetEndpoint: target.endpointCandidates[0],
4775
+ targetEndpointCandidates: target.endpointCandidates,
4776
+ targetLeaseId: target.leaseId,
4777
+ targetNodeId: target.nodeId,
4778
+ targetExpiresAt: target.expiresAt
4779
+ });
4780
+
4781
+ scheduleRemoteRegistryFollower(ok ? REMOTE_REGISTRY_FOLLOWER_POLL_MS : REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS, ok ? 'connected' : 'connect-failed');
4782
+ return serializeRemoteRegistryFollowerState();
4783
+ } catch (error) {
4784
+ const transientAuth = error?.statusCode === 401 || error?.statusCode === 403;
4785
+ updateRemoteRegistryFollowerState({
4786
+ status: transientAuth ? 'auth-pending' : 'error',
4787
+ reason: transientAuth ? 'registry-auth-pending' : 'registry-sync-failed',
4788
+ authenticated: false,
4789
+ lastAttemptAt: attemptedAt,
4790
+ lastError: error?.message || String(error || '')
4791
+ });
4792
+ await reportRemoteRegistryFollowerSync({
4793
+ ok: isRemoteAgentProcessRunning(),
4794
+ skipped: true,
4795
+ reason: transientAuth ? 'registry-auth-pending-agent-kept' : 'registry-sync-failed',
4796
+ error: error?.message || String(error || ''),
4797
+ trigger,
4798
+ authenticated: false
4799
+ });
4800
+ scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS, transientAuth ? 'auth-pending' : 'sync-error');
4801
+ return serializeRemoteRegistryFollowerState();
4802
+ } finally {
4803
+ remoteRegistryFollowerInFlight = false;
4804
+ }
4805
+ }
4806
+
4807
+ function startRemoteRegistryFollower() {
4808
+ if (!REMOTE_REGISTRY_FOLLOWER_ENABLED || remoteRegistryFollowerStarted) {
4809
+ return;
4810
+ }
4811
+
4812
+ remoteRegistryFollowerStarted = true;
4813
+ updateRemoteRegistryFollowerState({
4814
+ status: 'starting',
4815
+ reason: 'startup',
4816
+ lastAttemptAt: '',
4817
+ lastError: ''
4818
+ });
4819
+ scheduleRemoteRegistryFollower(250, 'startup');
4820
+ }
4821
+
4822
+ function stopRemoteRegistryFollower() {
4823
+ if (remoteRegistryFollowerTimer) {
4824
+ clearTimeout(remoteRegistryFollowerTimer);
4825
+ remoteRegistryFollowerTimer = null;
4826
+ }
4827
+ remoteRegistryFollowerStarted = false;
4828
+ updateRemoteRegistryFollowerState({
4829
+ status: REMOTE_REGISTRY_FOLLOWER_ENABLED ? 'stopped' : 'disabled',
4830
+ reason: 'bridge-shutdown'
4831
+ });
4832
+ }
4833
+
4157
4834
  function trimShellOutput(value, maxLength) {
4158
4835
  const text = String(value || '');
4159
4836
  if (text.length <= maxLength) {
4160
- return text;
4161
- }
4837
+ return text;
4838
+ }
4162
4839
 
4163
4840
  return text.slice(text.length - maxLength);
4164
4841
  }
@@ -8610,6 +9287,7 @@ app.get('/api/status', async (req, res) => {
8610
9287
  bridgeAuthRequired,
8611
9288
  remoteHub: remoteHub.getStatus({ includeSecrets: false }),
8612
9289
  remoteAgent: serializeRemoteAgentState(),
9290
+ remoteRegistryFollower: serializeRemoteRegistryFollowerState(),
8613
9291
  shellJobsPath: '/api/shell/jobs',
8614
9292
  companyCore: {
8615
9293
  baseUrl: companyCoreBaseUrl,
@@ -10400,15 +11078,14 @@ async function startBridgeServer() {
10400
11078
  `${tone('listen', 'muted')} ${tone(`http://127.0.0.1:${PORT}`, 'path')}`,
10401
11079
  `${tone('stop', 'muted')} Ctrl+C`
10402
11080
  ]);
11081
+ startRemoteRegistryFollower();
10403
11082
  });
10404
11083
  }
10405
11084
 
10406
11085
  await startBridgeServer();
10407
11086
 
10408
- // Graceful shutdown
10409
- let isShuttingDown = false;
10410
-
10411
- async function shutdownBridge(signal) {
11087
+ // Graceful shutdown
11088
+ async function shutdownBridge(signal) {
10412
11089
  if (isShuttingDown) return;
10413
11090
  isShuttingDown = true;
10414
11091
 
@@ -10435,6 +11112,12 @@ async function shutdownBridge(signal) {
10435
11112
  // Ignore if already closed
10436
11113
  }
10437
11114
 
11115
+ try {
11116
+ stopRemoteRegistryFollower();
11117
+ } catch {
11118
+ // Ignore registry follower shutdown errors
11119
+ }
11120
+
10438
11121
  try {
10439
11122
  await stopRemoteAgentConnection('bridge-shutdown');
10440
11123
  } catch {