@mindexec/cli 0.2.186 → 0.2.187

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.186",
3
+ "version": "0.2.187",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -15,6 +15,9 @@ const PAIR_TOKEN = 'remote-registry-follower-pair-token';
15
15
  const USER_ID = '11111111-2222-4333-8444-555555555555';
16
16
  const SUPABASE_KEY = 'remote-registry-follower-key';
17
17
  const ACCESS_TOKEN = 'remote-registry-follower-access-token';
18
+ const EXPIRED_ACCESS_TOKEN = 'remote-registry-follower-expired-access-token';
19
+ const REFRESHED_ACCESS_TOKEN = 'remote-registry-follower-refreshed-access-token';
20
+ const REFRESH_TOKEN = 'remote-registry-follower-refresh-token';
18
21
  const LOCAL_BRIDGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
22
 
20
23
  function wait(ms) {
@@ -86,6 +89,14 @@ function createRegistryTarget({ endpoint, endpointCandidates, leaseId, active =
86
89
  function startFakeSupabase(getTarget, setTarget = null) {
87
90
  const requests = [];
88
91
  const realtimeClients = new Set();
92
+ const validAccessTokens = new Set([ACCESS_TOKEN]);
93
+ const assertRegistryAuth = req => {
94
+ assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
95
+ const authorization = String(req.headers.authorization || '');
96
+ assert.ok(
97
+ validAccessTokens.has(authorization.replace(/^Bearer\s+/i, '')),
98
+ `unexpected registry authorization: ${authorization}`);
99
+ };
89
100
  const server = createServer(async (req, res) => {
90
101
  const readBody = () => new Promise(resolve => {
91
102
  let body = '';
@@ -103,8 +114,7 @@ function startFakeSupabase(getTarget, setTarget = null) {
103
114
  });
104
115
 
105
116
  if (req.method === 'GET' && parsed.pathname === '/rest/v1/remote_host_targets') {
106
- assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
107
- assert.equal(String(req.headers.authorization || ''), `Bearer ${ACCESS_TOKEN}`);
117
+ assertRegistryAuth(req);
108
118
  res.writeHead(200, {
109
119
  'Content-Type': 'application/json',
110
120
  'Cache-Control': 'no-store'
@@ -115,8 +125,7 @@ function startFakeSupabase(getTarget, setTarget = null) {
115
125
  }
116
126
 
117
127
  if (req.method === 'POST' && parsed.pathname === '/rest/v1/rpc/set_remote_host_target') {
118
- assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
119
- assert.equal(String(req.headers.authorization || ''), `Bearer ${ACCESS_TOKEN}`);
128
+ assertRegistryAuth(req);
120
129
  const body = JSON.parse(await readBody() || '{}');
121
130
  const now = Date.now();
122
131
  const existing = getTarget();
@@ -148,6 +157,31 @@ function startFakeSupabase(getTarget, setTarget = null) {
148
157
  return;
149
158
  }
150
159
 
160
+ if (req.method === 'POST' && parsed.pathname === '/auth/v1/token') {
161
+ assert.equal(parsed.searchParams.get('grant_type'), 'refresh_token');
162
+ assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
163
+ assert.equal(String(req.headers.authorization || ''), `Bearer ${SUPABASE_KEY}`);
164
+ const body = JSON.parse(await readBody() || '{}');
165
+ assert.equal(body.refresh_token, REFRESH_TOKEN);
166
+ validAccessTokens.add(REFRESHED_ACCESS_TOKEN);
167
+ res.writeHead(200, {
168
+ 'Content-Type': 'application/json',
169
+ 'Cache-Control': 'no-store'
170
+ });
171
+ res.end(JSON.stringify({
172
+ access_token: REFRESHED_ACCESS_TOKEN,
173
+ refresh_token: REFRESH_TOKEN,
174
+ token_type: 'bearer',
175
+ expires_in: 3600,
176
+ expires_at: Math.floor(Date.now() / 1000) + 3600,
177
+ user: {
178
+ id: USER_ID,
179
+ email: 'remote-registry-follower@example.test'
180
+ }
181
+ }));
182
+ return;
183
+ }
184
+
151
185
  res.writeHead(404, { 'Content-Type': 'application/json' });
152
186
  res.end(JSON.stringify({ error: 'not-found' }));
153
187
  });
@@ -166,8 +200,8 @@ function startFakeSupabase(getTarget, setTarget = null) {
166
200
 
167
201
  if (message.event === 'phx_join') {
168
202
  assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
169
- assert.equal(String(req.headers.authorization || ''), `Bearer ${ACCESS_TOKEN}`);
170
- assert.equal(message.payload?.access_token, ACCESS_TOKEN);
203
+ assert.ok(validAccessTokens.has(String(req.headers.authorization || '').replace(/^Bearer\s+/i, '')));
204
+ assert.ok(validAccessTokens.has(String(message.payload?.access_token || '')));
171
205
  assert.deepEqual(
172
206
  message.payload?.config?.postgres_changes?.[0],
173
207
  {
@@ -338,10 +372,14 @@ async function waitForBridge(bridge) {
338
372
  }, 30000, `bridge startup\n${bridge.details()}`);
339
373
  }
340
374
 
341
- function createSessionPayload() {
375
+ function createSessionPayload(options = {}) {
376
+ const accessToken = options.accessToken || ACCESS_TOKEN;
377
+ const refreshToken = options.refreshToken || REFRESH_TOKEN;
378
+ const expiresAt = options.expiresAt ?? Math.floor(Date.now() / 1000) + 3600;
342
379
  return JSON.stringify({
343
- access_token: ACCESS_TOKEN,
344
- expires_at: Math.floor(Date.now() / 1000) + 3600,
380
+ access_token: accessToken,
381
+ refresh_token: refreshToken,
382
+ expires_at: expiresAt,
345
383
  user: {
346
384
  id: USER_ID,
347
385
  email: 'remote-registry-follower@example.test'
@@ -349,9 +387,9 @@ function createSessionPayload() {
349
387
  });
350
388
  }
351
389
 
352
- async function writeSession(authRoot) {
390
+ async function writeSession(authRoot, options = {}) {
353
391
  await mkdir(authRoot, { recursive: true });
354
- await writeFile(path.join(authRoot, 'supabase-session.json'), createSessionPayload(), 'utf8');
392
+ await writeFile(path.join(authRoot, 'supabase-session.json'), createSessionPayload(options), 'utf8');
355
393
  }
356
394
 
357
395
  async function waitForManagedAgent(bridge, managerEndpoint, label, timeoutMs = 20000) {
@@ -435,7 +473,10 @@ async function main() {
435
473
  const hostBAuth = path.join(tempRoot, 'auth-host-b');
436
474
  const clientAuth = path.join(tempRoot, 'auth-client');
437
475
  await writeSession(renewHostAuth);
438
- await writeSession(clientAuth);
476
+ await writeSession(clientAuth, {
477
+ accessToken: EXPIRED_ACCESS_TOKEN,
478
+ expiresAt: Math.floor(Date.now() / 1000) - 30
479
+ });
439
480
 
440
481
  renewHost = spawnBridge({
441
482
  bridgePort: renewHostBridgePort,
@@ -544,6 +585,17 @@ async function main() {
544
585
  assert.match(String(firstAgent.launcher || ''), /mindexec-remote-fast/i);
545
586
  assert.equal(firstAgent.manager, hostAEndpoint);
546
587
  assert.equal(firstAgent.managerCandidates?.[0], staleEndpoint, JSON.stringify(firstAgent.managerCandidates));
588
+ assert.ok(
589
+ fakeSupabase.requests.some(request =>
590
+ request.method === 'POST'
591
+ && request.pathname === '/auth/v1/token'),
592
+ 'expected LocalBridge to refresh expired persisted Supabase session without browser involvement');
593
+ assert.ok(
594
+ fakeSupabase.requests.some(request =>
595
+ request.method === 'GET'
596
+ && request.pathname === '/rest/v1/remote_host_targets'
597
+ && request.authorization === `Bearer ${REFRESHED_ACCESS_TOKEN}`),
598
+ 'expected registry read to use refreshed access token');
547
599
  await waitForConnectedDevice(hostA, 'host-a');
548
600
  const firstStatus = await fetchJson(`${client.baseUrl}/api/status`);
549
601
  assert.equal(firstStatus.payload?.remoteRegistryRealtime?.subscribed, true, JSON.stringify(firstStatus.payload?.remoteRegistryRealtime));
package/server.js CHANGED
@@ -3388,6 +3388,7 @@ const REMOTE_REGISTRY_FOLLOWER_MISSING_SESSION_FAST_RETRY_COUNT = Math.max(
3388
3388
  Number(process.env.MINDEXEC_REMOTE_REGISTRY_MISSING_SESSION_FAST_RETRY_COUNT || 5) || 5);
3389
3389
  const REMOTE_REGISTRY_FOLLOWER_INACTIVE_STOP_COUNT = Math.max(2, Number(process.env.MINDEXEC_REMOTE_REGISTRY_INACTIVE_STOP_COUNT || 8) || 8);
3390
3390
  const REMOTE_REGISTRY_FOLLOWER_INACTIVE_GRACE_MS = Math.max(500, Number(process.env.MINDEXEC_REMOTE_REGISTRY_INACTIVE_GRACE_MS || 60000) || 60000);
3391
+ const REMOTE_REGISTRY_SESSION_REFRESH_LEAD_MS = Math.max(15_000, Number(process.env.MINDEXEC_REMOTE_REGISTRY_SESSION_REFRESH_LEAD_MS || 120_000) || 120_000);
3391
3392
  const REMOTE_REGISTRY_REALTIME_ENABLED = REMOTE_REGISTRY_FOLLOWER_ENABLED
3392
3393
  && !/^(0|false|no|off)$/i.test(String(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME || 'true'));
3393
3394
  const REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS = Math.max(10000, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS || 25000) || 25000);
@@ -3426,6 +3427,7 @@ let remoteRegistryFollowerStarted = false;
3426
3427
  let remoteRegistryFollowerConsecutiveMissingSession = 0;
3427
3428
  let remoteRegistryFollowerConsecutiveInactiveTarget = 0;
3428
3429
  let remoteRegistryFollowerInactiveTargetFirstAt = 0;
3430
+ let remoteRegistrySessionRefreshPromise = null;
3429
3431
  let remoteRegistryRealtimeSocket = null;
3430
3432
  let remoteRegistryRealtimeSessionKey = '';
3431
3433
  let remoteRegistryRealtimeTopic = '';
@@ -5533,6 +5535,75 @@ function readSupabaseSessionField(source, ...keys) {
5533
5535
  return '';
5534
5536
  }
5535
5537
 
5538
+ function parseSupabaseSessionExpiresAtMs(value) {
5539
+ if (value === undefined || value === null || String(value).trim() === '') {
5540
+ return 0;
5541
+ }
5542
+
5543
+ const numeric = Number(value);
5544
+ if (Number.isFinite(numeric) && numeric > 0) {
5545
+ return numeric > 9999999999 ? numeric : numeric * 1000;
5546
+ }
5547
+
5548
+ const parsed = Date.parse(String(value));
5549
+ return Number.isFinite(parsed) ? parsed : 0;
5550
+ }
5551
+
5552
+ function setSupabaseSessionKnownField(session, value, fallbackKey, ...keys) {
5553
+ let wrote = false;
5554
+ for (const key of keys) {
5555
+ if (Object.prototype.hasOwnProperty.call(session, key)) {
5556
+ session[key] = value;
5557
+ wrote = true;
5558
+ }
5559
+ }
5560
+
5561
+ if (!wrote) {
5562
+ session[fallbackKey] = value;
5563
+ }
5564
+ }
5565
+
5566
+ function buildRefreshedSupabaseSessionPayload(content, refreshedSession) {
5567
+ const existing = JSON.parse(String(content || '{}'));
5568
+ const accessToken = String(readSupabaseSessionField(refreshedSession, 'access_token', 'accessToken', 'AccessToken')).trim();
5569
+ const refreshToken = String(readSupabaseSessionField(refreshedSession, 'refresh_token', 'refreshToken', 'RefreshToken')).trim()
5570
+ || String(readSupabaseSessionField(existing, 'refresh_token', 'refreshToken', 'RefreshToken')).trim();
5571
+ const tokenType = String(readSupabaseSessionField(refreshedSession, 'token_type', 'tokenType', 'TokenType')).trim()
5572
+ || String(readSupabaseSessionField(existing, 'token_type', 'tokenType', 'TokenType')).trim()
5573
+ || 'bearer';
5574
+ const expiresInRaw = Number(readSupabaseSessionField(refreshedSession, 'expires_in', 'expiresIn', 'ExpiresIn') || 0);
5575
+ const expiresAtFromResponse = readSupabaseSessionField(refreshedSession, 'expires_at', 'expiresAt', 'ExpiresAt');
5576
+ const expiresAtMs = parseSupabaseSessionExpiresAtMs(expiresAtFromResponse)
5577
+ || (Number.isFinite(expiresInRaw) && expiresInRaw > 0 ? Date.now() + (expiresInRaw * 1000) : 0);
5578
+ const expiresAtSeconds = expiresAtMs > 0 ? Math.floor(expiresAtMs / 1000) : 0;
5579
+
5580
+ if (!accessToken || !refreshToken || !expiresAtSeconds) {
5581
+ const error = new Error('auth-refresh-response-incomplete');
5582
+ error.statusCode = 502;
5583
+ throw error;
5584
+ }
5585
+
5586
+ setSupabaseSessionKnownField(existing, accessToken, 'access_token', 'access_token', 'accessToken', 'AccessToken');
5587
+ setSupabaseSessionKnownField(existing, refreshToken, 'refresh_token', 'refresh_token', 'refreshToken', 'RefreshToken');
5588
+ setSupabaseSessionKnownField(existing, tokenType, 'token_type', 'token_type', 'tokenType', 'TokenType');
5589
+ setSupabaseSessionKnownField(existing, expiresAtSeconds, 'expires_at', 'expires_at', 'expiresAt', 'ExpiresAt');
5590
+
5591
+ if (Number.isFinite(expiresInRaw) && expiresInRaw > 0) {
5592
+ setSupabaseSessionKnownField(existing, Math.floor(expiresInRaw), 'expires_in', 'expires_in', 'expiresIn', 'ExpiresIn');
5593
+ }
5594
+
5595
+ const user = refreshedSession?.user || refreshedSession?.User;
5596
+ if (user && typeof user === 'object') {
5597
+ if (Object.prototype.hasOwnProperty.call(existing, 'User')) {
5598
+ existing.User = user;
5599
+ } else {
5600
+ existing.user = user;
5601
+ }
5602
+ }
5603
+
5604
+ return JSON.stringify(existing);
5605
+ }
5606
+
5536
5607
  function parseSupabaseSessionForRegistry(content) {
5537
5608
  let session = null;
5538
5609
  try {
@@ -5543,13 +5614,11 @@ function parseSupabaseSessionForRegistry(content) {
5543
5614
 
5544
5615
  const user = session?.user || session?.User || {};
5545
5616
  const accessToken = String(readSupabaseSessionField(session, 'access_token', 'accessToken', 'AccessToken')).trim();
5617
+ const refreshToken = String(readSupabaseSessionField(session, 'refresh_token', 'refreshToken', 'RefreshToken')).trim();
5546
5618
  const userId = String(
5547
5619
  readSupabaseSessionField(user, 'id', 'Id')
5548
5620
  || readSupabaseSessionField(session, 'user_id', 'userId', 'UserId')).trim();
5549
- const expiresAtRaw = Number(readSupabaseSessionField(session, 'expires_at', 'expiresAt', 'ExpiresAt') || 0);
5550
- const expiresAtMs = Number.isFinite(expiresAtRaw) && expiresAtRaw > 0
5551
- ? (expiresAtRaw > 9999999999 ? expiresAtRaw : expiresAtRaw * 1000)
5552
- : 0;
5621
+ const expiresAtMs = parseSupabaseSessionExpiresAtMs(readSupabaseSessionField(session, 'expires_at', 'expiresAt', 'ExpiresAt'));
5553
5622
 
5554
5623
  if (!accessToken || !userId) {
5555
5624
  return null;
@@ -5557,11 +5626,143 @@ function parseSupabaseSessionForRegistry(content) {
5557
5626
 
5558
5627
  return {
5559
5628
  accessToken,
5629
+ refreshToken,
5560
5630
  userId,
5561
5631
  expiresAtMs
5562
5632
  };
5563
5633
  }
5564
5634
 
5635
+ async function refreshSupabaseSessionForRegistry(config, sessionPayload, reason = 'registry') {
5636
+ const currentSession = parseSupabaseSessionForRegistry(sessionPayload?.content);
5637
+ if (!currentSession?.refreshToken) {
5638
+ return {
5639
+ ok: false,
5640
+ reason: 'refresh-token-missing'
5641
+ };
5642
+ }
5643
+
5644
+ if (remoteRegistrySessionRefreshPromise) {
5645
+ return await remoteRegistrySessionRefreshPromise;
5646
+ }
5647
+
5648
+ remoteRegistrySessionRefreshPromise = (async () => {
5649
+ const url = new URL('/auth/v1/token', config.url);
5650
+ url.searchParams.set('grant_type', 'refresh_token');
5651
+ const response = await fetch(url.toString(), {
5652
+ method: 'POST',
5653
+ headers: {
5654
+ apikey: config.key,
5655
+ Authorization: `Bearer ${config.key}`,
5656
+ Accept: 'application/json',
5657
+ 'Content-Type': 'application/json'
5658
+ },
5659
+ body: JSON.stringify({
5660
+ refresh_token: currentSession.refreshToken
5661
+ })
5662
+ });
5663
+
5664
+ if (!response.ok) {
5665
+ const text = await response.text().catch(() => '');
5666
+ const error = new Error(`auth-refresh-${response.status}${text ? `:${shortenText(text, 160)}` : ''}`);
5667
+ error.statusCode = response.status;
5668
+ throw error;
5669
+ }
5670
+
5671
+ const refreshed = await response.json();
5672
+ const content = buildRefreshedSupabaseSessionPayload(sessionPayload.content, refreshed);
5673
+ await writeStableAuthSessionPayload(content);
5674
+ const session = parseSupabaseSessionForRegistry(content);
5675
+ if (!session) {
5676
+ const error = new Error('auth-refresh-session-parse-failed');
5677
+ error.statusCode = 502;
5678
+ throw error;
5679
+ }
5680
+
5681
+ logEvent(
5682
+ 'remote',
5683
+ `registry auth refreshed ${formatKeyValue('reason', reason || 'registry')} ${formatKeyValue('expiresInMs', Math.max(0, session.expiresAtMs - Date.now()))}`,
5684
+ 'remote');
5685
+
5686
+ return {
5687
+ ok: true,
5688
+ reason: 'refreshed',
5689
+ content,
5690
+ session
5691
+ };
5692
+ })();
5693
+
5694
+ try {
5695
+ return await remoteRegistrySessionRefreshPromise;
5696
+ } finally {
5697
+ remoteRegistrySessionRefreshPromise = null;
5698
+ }
5699
+ }
5700
+
5701
+ async function readFreshSupabaseSessionForRegistry(config, reason = 'registry') {
5702
+ const sessionPayload = await readStableAuthSessionPayload();
5703
+ const session = sessionPayload.found ? parseSupabaseSessionForRegistry(sessionPayload.content) : null;
5704
+ if (!session) {
5705
+ return {
5706
+ found: sessionPayload.found,
5707
+ payload: sessionPayload,
5708
+ session: null,
5709
+ refreshed: false,
5710
+ expired: false
5711
+ };
5712
+ }
5713
+
5714
+ const now = Date.now();
5715
+ const shouldRefresh = session.expiresAtMs > 0
5716
+ && session.expiresAtMs <= now + REMOTE_REGISTRY_SESSION_REFRESH_LEAD_MS;
5717
+ if (!shouldRefresh) {
5718
+ return {
5719
+ found: true,
5720
+ payload: sessionPayload,
5721
+ session,
5722
+ refreshed: false,
5723
+ expired: session.expiresAtMs > 0 && session.expiresAtMs <= now
5724
+ };
5725
+ }
5726
+
5727
+ try {
5728
+ const refreshed = await refreshSupabaseSessionForRegistry(config, sessionPayload, reason);
5729
+ if (refreshed.ok && refreshed.session) {
5730
+ return {
5731
+ found: true,
5732
+ payload: {
5733
+ ...sessionPayload,
5734
+ content: refreshed.content,
5735
+ refreshed: true
5736
+ },
5737
+ session: refreshed.session,
5738
+ refreshed: true,
5739
+ expired: false
5740
+ };
5741
+ }
5742
+
5743
+ return {
5744
+ found: true,
5745
+ payload: sessionPayload,
5746
+ session,
5747
+ refreshed: false,
5748
+ expired: session.expiresAtMs > 0 && session.expiresAtMs <= Date.now(),
5749
+ refreshError: refreshed.reason || 'auth-refresh-skipped'
5750
+ };
5751
+ } catch (error) {
5752
+ logWarn(
5753
+ 'remote',
5754
+ `registry auth refresh failed ${formatKeyValue('reason', reason || 'registry')} ${formatKeyValue('error', error?.message || String(error || ''))}`);
5755
+ return {
5756
+ found: true,
5757
+ payload: sessionPayload,
5758
+ session,
5759
+ refreshed: false,
5760
+ expired: session.expiresAtMs > 0 && session.expiresAtMs <= Date.now(),
5761
+ refreshError: error?.message || String(error || '')
5762
+ };
5763
+ }
5764
+ }
5765
+
5565
5766
  function readRegistryTargetField(target, ...keys) {
5566
5767
  for (const key of keys) {
5567
5768
  const value = target?.[key];
@@ -5700,13 +5901,13 @@ async function readRemoteRegistryContext() {
5700
5901
  return { ok: false, reason: 'supabase-config-missing' };
5701
5902
  }
5702
5903
 
5703
- const sessionPayload = await readStableAuthSessionPayload();
5704
- const session = sessionPayload.found ? parseSupabaseSessionForRegistry(sessionPayload.content) : null;
5904
+ const sessionState = await readFreshSupabaseSessionForRegistry(config, 'registry-context');
5905
+ const session = sessionState.session;
5705
5906
  if (!session) {
5706
5907
  return { ok: false, reason: 'registry-not-authenticated' };
5707
5908
  }
5708
5909
 
5709
- if (session.expiresAtMs && session.expiresAtMs <= Date.now()) {
5910
+ if (sessionState.expired || (session.expiresAtMs && session.expiresAtMs <= Date.now())) {
5710
5911
  return { ok: false, reason: 'session-expired' };
5711
5912
  }
5712
5913
 
@@ -6321,8 +6522,8 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
6321
6522
  return serializeRemoteRegistryFollowerState();
6322
6523
  }
6323
6524
 
6324
- const sessionPayload = await readStableAuthSessionPayload();
6325
- const session = sessionPayload.found ? parseSupabaseSessionForRegistry(sessionPayload.content) : null;
6525
+ const sessionState = await readFreshSupabaseSessionForRegistry(config, `follower-${trigger || 'timer'}`);
6526
+ const session = sessionState.session;
6326
6527
  if (!session) {
6327
6528
  closeRemoteRegistryRealtime('registry-not-authenticated');
6328
6529
  remoteRegistryFollowerConsecutiveMissingSession += 1;
@@ -6368,23 +6569,39 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
6368
6569
  }
6369
6570
 
6370
6571
  remoteRegistryFollowerConsecutiveMissingSession = 0;
6371
- if (session.expiresAtMs && session.expiresAtMs <= Date.now()) {
6572
+ if (sessionState.expired || (session.expiresAtMs && session.expiresAtMs <= Date.now())) {
6372
6573
  closeRemoteRegistryRealtime('session-expired');
6574
+ const agentKept = isRemoteAgentRegistryOwned() && isRemoteAgentProcessRunning();
6575
+ const reason = agentKept
6576
+ ? 'session-expired-agent-kept'
6577
+ : 'session-expired';
6578
+ const keptManager = agentKept ? safeRemoteAgentField(remoteAgentState.manager, 160) : '';
6579
+ const keptCandidates = agentKept ? normalizeRemoteManagerEndpointList(remoteAgentState.managerCandidates, remoteAgentState.manager) : [];
6373
6580
  updateRemoteRegistryFollowerState({
6374
- status: 'skipped',
6375
- reason: 'session-expired',
6581
+ status: agentKept ? 'auth-pending' : 'skipped',
6582
+ reason,
6376
6583
  authenticated: false,
6377
6584
  lastAttemptAt: attemptedAt,
6378
- lastError: ''
6585
+ lastError: sessionState.refreshError || '',
6586
+ targetEndpoint: keptManager,
6587
+ targetEndpointCandidates: keptCandidates,
6588
+ targetLeaseId: agentKept ? safeRemoteAgentField(remoteAgentState.leaseId, 128) : '',
6589
+ targetNodeId: agentKept ? safeRemoteAgentField(remoteAgentState.nodeId, 128) : '',
6590
+ agentKept
6379
6591
  });
6380
6592
  await reportRemoteRegistryFollowerSync({
6381
6593
  ok: true,
6382
6594
  skipped: true,
6383
- reason: 'session-expired',
6595
+ reason,
6384
6596
  trigger,
6385
- authenticated: false
6597
+ authenticated: false,
6598
+ targetEndpoint: keptManager,
6599
+ targetEndpointCandidates: keptCandidates,
6600
+ targetLeaseId: agentKept ? remoteAgentState.leaseId : '',
6601
+ targetNodeId: agentKept ? remoteAgentState.nodeId : '',
6602
+ agentKept
6386
6603
  });
6387
- scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'session-expired');
6604
+ scheduleRemoteRegistryFollower(agentKept ? REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS : REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'session-expired');
6388
6605
  return serializeRemoteRegistryFollowerState();
6389
6606
  }
6390
6607