@mindexec/cli 0.2.150 → 0.2.152

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/codex-runtime.js CHANGED
@@ -38,6 +38,11 @@ const MAX_LOG_CHARS = 4000;
38
38
  const MAX_EVENT_LOG = 120;
39
39
  const TEMP_DIR = '.ai/codex';
40
40
  const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
41
+ const CODEX_SOURCE_HOME = path.join(os.homedir(), '.codex');
42
+ const CODEX_RUNTIME_HOME_ENV = 'MINDEXEC_CODEX_HOME';
43
+ const DEFAULT_CODEX_RUNTIME_HOME = path.join(os.homedir(), '.mindexec', 'codex-runtime');
44
+ const CODEX_RUNTIME_CONFIG_MARKER = '# Generated by MindExec LocalBridge for isolated AI node runs.';
45
+ const CODEX_RUNTIME_AUTH_FILES = ['auth.json'];
41
46
 
42
47
  let cachedSdkModule = null;
43
48
  let cachedSdkLoadError = null;
@@ -153,6 +158,70 @@ function buildCodexSdkConfigOverrides() {
153
158
  return { mcp_servers: mcpServers };
154
159
  }
155
160
 
161
+ function resolveCodexRuntimeHome() {
162
+ const configured = String(process.env[CODEX_RUNTIME_HOME_ENV] || '').trim();
163
+ return path.resolve(configured || DEFAULT_CODEX_RUNTIME_HOME);
164
+ }
165
+
166
+ async function copyCodexRuntimeFileIfPresent(fileName, runtimeHome) {
167
+ const source = path.join(CODEX_SOURCE_HOME, fileName);
168
+ const target = path.join(runtimeHome, fileName);
169
+ try {
170
+ const sourceStat = await fs.stat(source);
171
+ if (!sourceStat.isFile()) {
172
+ return false;
173
+ }
174
+
175
+ await fs.copyFile(source, target);
176
+ return true;
177
+ } catch {
178
+ return false;
179
+ }
180
+ }
181
+
182
+ async function ensureCodexRuntimeHome() {
183
+ const runtimeHome = resolveCodexRuntimeHome();
184
+ await fs.mkdir(runtimeHome, { recursive: true });
185
+ await fs.mkdir(path.join(runtimeHome, 'sessions'), { recursive: true });
186
+ await fs.mkdir(path.join(runtimeHome, 'generated_images'), { recursive: true });
187
+
188
+ for (const fileName of CODEX_RUNTIME_AUTH_FILES) {
189
+ await copyCodexRuntimeFileIfPresent(fileName, runtimeHome);
190
+ }
191
+
192
+ const configPath = path.join(runtimeHome, 'config.toml');
193
+ let shouldWriteConfig = true;
194
+ try {
195
+ const existing = await fs.readFile(configPath, 'utf8');
196
+ shouldWriteConfig = existing.trim().length === 0 || existing.includes(CODEX_RUNTIME_CONFIG_MARKER);
197
+ } catch {
198
+ shouldWriteConfig = true;
199
+ }
200
+
201
+ if (shouldWriteConfig) {
202
+ await fs.writeFile(
203
+ configPath,
204
+ `${CODEX_RUNTIME_CONFIG_MARKER}\n# User MCP server definitions are intentionally not inherited here.\n# LocalBridge passes model, sandbox, and reasoning options per run.\n`,
205
+ 'utf8'
206
+ );
207
+ }
208
+
209
+ return runtimeHome;
210
+ }
211
+
212
+ function buildCodexChildEnv() {
213
+ const env = {};
214
+ for (const [key, value] of Object.entries(process.env)) {
215
+ if (value !== undefined) {
216
+ env[key] = value;
217
+ }
218
+ }
219
+
220
+ env.CODEX_HOME = resolveCodexRuntimeHome();
221
+ env.MINDEXEC_CODEX_ISOLATED_HOME = '1';
222
+ return env;
223
+ }
224
+
156
225
  function appendCodexIsolationConfigArgs(args) {
157
226
  for (const name of readConfiguredMcpServerNames()) {
158
227
  args.push('--config', `mcp_servers.${name}.enabled=false`);
@@ -562,7 +631,11 @@ export function createCodexRuntime(options) {
562
631
 
563
632
  if (providerKind === PROVIDER_KIND.typeScriptSdk) {
564
633
  const sdk = await loadCodexSdk();
565
- const codex = new sdk.Codex({ config: buildCodexSdkConfigOverrides() });
634
+ await ensureCodexRuntimeHome();
635
+ const codex = new sdk.Codex({
636
+ env: buildCodexChildEnv(),
637
+ config: buildCodexSdkConfigOverrides()
638
+ });
566
639
  const thread = codex.startThread(threadOptions);
567
640
  const localThreadId = `local_${crypto.randomUUID()}`;
568
641
  threads.set(localThreadId, {
@@ -624,7 +697,11 @@ export function createCodexRuntime(options) {
624
697
  }
625
698
 
626
699
  const sdk = await loadCodexSdk();
627
- const codex = new sdk.Codex({ config: buildCodexSdkConfigOverrides() });
700
+ await ensureCodexRuntimeHome();
701
+ const codex = new sdk.Codex({
702
+ env: buildCodexChildEnv(),
703
+ config: buildCodexSdkConfigOverrides()
704
+ });
628
705
  const officialId = requestedThreadId && !requestedThreadId.startsWith('local_')
629
706
  ? requestedThreadId
630
707
  : '';
@@ -765,6 +842,7 @@ export function createCodexRuntime(options) {
765
842
  const tempDir = path.join(workingDirectory, TEMP_DIR);
766
843
  const schema = normalizeOutputSchema(body.outputSchema || body.outputSchemaJson);
767
844
  let schemaPath = null;
845
+ await ensureCodexRuntimeHome();
768
846
  const args = [
769
847
  'exec',
770
848
  '-',
@@ -806,6 +884,7 @@ export function createCodexRuntime(options) {
806
884
  const childResult = await new Promise((resolve) => {
807
885
  const child = spawn('codex', args, {
808
886
  cwd: workingDirectory,
887
+ env: buildCodexChildEnv(),
809
888
  windowsHide: true,
810
889
  signal: abortController.signal
811
890
  });
@@ -935,7 +1014,11 @@ export function createCodexRuntime(options) {
935
1014
  const workingDirectory = await resolveWorkingDirectory(body.workingDir || body.workingDirectory || '');
936
1015
  const threadOptions = buildThreadOptions(body, workingDirectory);
937
1016
  const sdk = await loadCodexSdk();
938
- const codex = new sdk.Codex({ config: buildCodexSdkConfigOverrides() });
1017
+ await ensureCodexRuntimeHome();
1018
+ const codex = new sdk.Codex({
1019
+ env: buildCodexChildEnv(),
1020
+ config: buildCodexSdkConfigOverrides()
1021
+ });
939
1022
  const thread = codex.resumeThread(threadId, threadOptions);
940
1023
  threads.set(threadId, {
941
1024
  providerKind,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.150",
3
+ "version": "0.2.152",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -83,10 +83,17 @@ function createRegistryTarget({ endpoint, endpointCandidates, leaseId, active =
83
83
  };
84
84
  }
85
85
 
86
- function startFakeSupabase(getTarget) {
86
+ function startFakeSupabase(getTarget, setTarget = null) {
87
87
  const requests = [];
88
88
  const realtimeClients = new Set();
89
- const server = createServer((req, res) => {
89
+ const server = createServer(async (req, res) => {
90
+ const readBody = () => new Promise(resolve => {
91
+ let body = '';
92
+ req.on('data', chunk => {
93
+ body += chunk.toString();
94
+ });
95
+ req.on('end', () => resolve(body));
96
+ });
90
97
  const parsed = new URL(req.url || '/', 'http://127.0.0.1');
91
98
  requests.push({
92
99
  method: req.method,
@@ -107,6 +114,40 @@ function startFakeSupabase(getTarget) {
107
114
  return;
108
115
  }
109
116
 
117
+ 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}`);
120
+ const body = JSON.parse(await readBody() || '{}');
121
+ const now = Date.now();
122
+ const existing = getTarget();
123
+ const sameLease = existing
124
+ && String(existing.lease_id || '') === String(body.p_lease_id || '')
125
+ && String(existing.host_instance_id || '') === String(body.p_host_instance_id || '');
126
+ const expired = !existing?.expires_at || Date.parse(existing.expires_at) <= now;
127
+ if (existing?.active === true && !sameLease && !expired && body.p_takeover !== true) {
128
+ res.writeHead(200, { 'Content-Type': 'application/json' });
129
+ res.end(JSON.stringify([{ ok: false, reason: 'host-target-taken' }]));
130
+ return;
131
+ }
132
+
133
+ setTarget?.({
134
+ user_id: USER_ID,
135
+ active: true,
136
+ endpoint: String(body.p_endpoint || ''),
137
+ endpoint_candidates: Array.isArray(body.p_endpoint_candidates)
138
+ ? body.p_endpoint_candidates
139
+ : [String(body.p_endpoint || '')].filter(Boolean),
140
+ pair_token: String(body.p_pair_token || ''),
141
+ lease_id: String(body.p_lease_id || ''),
142
+ node_id: String(body.p_node_id || ''),
143
+ host_instance_id: String(body.p_host_instance_id || ''),
144
+ expires_at: String(body.p_expires_at || new Date(Date.now() + 60_000).toISOString())
145
+ });
146
+ res.writeHead(200, { 'Content-Type': 'application/json' });
147
+ res.end(JSON.stringify([{ ok: true, reason: 'ok' }]));
148
+ return;
149
+ }
150
+
110
151
  res.writeHead(404, { 'Content-Type': 'application/json' });
111
152
  res.end(JSON.stringify({ error: 'not-found' }));
112
153
  });
@@ -230,7 +271,7 @@ function startFakeSupabase(getTarget) {
230
271
  });
231
272
  }
232
273
 
233
- function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label, supabaseUrl = '', follower = false }) {
274
+ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label, supabaseUrl = '', follower = false, publicHost = '' }) {
234
275
  const child = spawn(process.execPath, ['server.js'], {
235
276
  cwd: LOCAL_BRIDGE_DIR,
236
277
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -243,6 +284,7 @@ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label
243
284
  MINDEXEC_REMOTE_HUB: '1',
244
285
  REMOTE_HUB_HOST: '127.0.0.1',
245
286
  REMOTE_HUB_PORT: String(remoteHubPort),
287
+ REMOTE_HUB_PUBLIC_HOST: publicHost,
246
288
  REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
247
289
  WORKSPACE_PATH: workspacePath,
248
290
  MINDEXEC_AUTH_DATA_ROOT: authRoot,
@@ -251,6 +293,7 @@ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label
251
293
  MINDEXEC_REMOTE_REGISTRY_FAST_RETRY_MS: '250',
252
294
  MINDEXEC_REMOTE_REGISTRY_REALTIME_RECONNECT_MS: '250',
253
295
  MINDEXEC_REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS: '100',
296
+ MINDEXEC_REMOTE_HOST_TARGET_RENEW_MS: '5000',
254
297
  SUPABASE_URL: supabaseUrl,
255
298
  SUPABASE_KEY,
256
299
  NO_COLOR: '1'
@@ -353,10 +396,13 @@ function killProcess(pid) {
353
396
  async function main() {
354
397
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'mindexec-remote-registry-smoke-'));
355
398
  let fakeSupabase = null;
399
+ let renewHost = null;
356
400
  let hostA = null;
357
401
  let hostB = null;
358
402
  let client = null;
359
403
 
404
+ const renewHostBridgePort = await findFreePort();
405
+ const renewHostRemotePort = await findFreePort();
360
406
  const hostABridgePort = await findFreePort();
361
407
  const hostARemotePort = await findFreePort();
362
408
  const hostBBridgePort = await findFreePort();
@@ -364,23 +410,82 @@ async function main() {
364
410
  const clientBridgePort = await findFreePort();
365
411
  const clientRemotePort = await findFreePort();
366
412
  const stalePort = await findFreePort();
413
+ const renewPublicHost = '192.0.2.10';
414
+ const renewPublicEndpoint = `${renewPublicHost}:${renewHostRemotePort}`;
367
415
  const hostAEndpoint = `127.0.0.1:${hostARemotePort}`;
368
416
  const hostBEndpoint = `127.0.0.1:${hostBRemotePort}`;
369
417
  const staleEndpoint = `127.0.0.1:${stalePort}`;
370
- let currentTarget = createRegistryTarget({
371
- endpoint: hostAEndpoint,
372
- endpointCandidates: [staleEndpoint, hostAEndpoint],
373
- leaseId: 'lease-a'
374
- });
418
+ let currentTarget = null;
375
419
 
376
420
  try {
377
- fakeSupabase = await startFakeSupabase(() => currentTarget);
421
+ fakeSupabase = await startFakeSupabase(
422
+ () => currentTarget,
423
+ target => {
424
+ currentTarget = target;
425
+ });
378
426
 
427
+ const renewHostAuth = path.join(tempRoot, 'auth-renew-host');
379
428
  const hostAAuth = path.join(tempRoot, 'auth-host-a');
380
429
  const hostBAuth = path.join(tempRoot, 'auth-host-b');
381
430
  const clientAuth = path.join(tempRoot, 'auth-client');
431
+ await writeSession(renewHostAuth);
382
432
  await writeSession(clientAuth);
383
433
 
434
+ renewHost = spawnBridge({
435
+ bridgePort: renewHostBridgePort,
436
+ remoteHubPort: renewHostRemotePort,
437
+ workspacePath: path.join(tempRoot, 'renew-host'),
438
+ authRoot: renewHostAuth,
439
+ label: 'renew-host',
440
+ supabaseUrl: fakeSupabase.url,
441
+ follower: true,
442
+ publicHost: renewPublicHost
443
+ });
444
+ await waitForBridge(renewHost);
445
+
446
+ const renewSetHost = await fetchJson(`${renewHost.baseUrl}/api/remote/host-target`, {
447
+ method: 'POST',
448
+ token: BRIDGE_TOKEN,
449
+ body: JSON.stringify({
450
+ nodeId: 'remote-registry-renew-node',
451
+ enabled: true,
452
+ leaseMs: 60000
453
+ })
454
+ });
455
+ assert.equal(renewSetHost.ok, true, JSON.stringify(renewSetHost.payload));
456
+ assert.equal(renewSetHost.payload?.ok, true, JSON.stringify(renewSetHost.payload));
457
+ assert.equal(renewSetHost.payload?.active, true, JSON.stringify(renewSetHost.payload));
458
+ assert.equal(renewSetHost.payload?.hostTargetRenew?.status, 'active', JSON.stringify(renewSetHost.payload?.hostTargetRenew));
459
+ assert.equal(currentTarget?.endpoint, renewPublicEndpoint, JSON.stringify(currentTarget));
460
+ assert.equal(currentTarget?.node_id, 'remote-registry-renew-node', JSON.stringify(currentTarget));
461
+ await waitFor(() => {
462
+ const publishCount = fakeSupabase.requests.filter(request =>
463
+ request.method === 'POST'
464
+ && request.pathname === '/rest/v1/rpc/set_remote_host_target').length;
465
+ return publishCount >= 2 ? publishCount : null;
466
+ }, 8000, `host-target auto renew publish\n${renewHost.details()}`);
467
+ const renewStatus = await fetchJson(`${renewHost.baseUrl}/api/status`);
468
+ assert.equal(renewStatus.payload?.remoteHostTargetRenew?.status, 'active', JSON.stringify(renewStatus.payload?.remoteHostTargetRenew));
469
+ assert.equal(renewStatus.payload?.remoteHostTargetRenew?.endpoint, renewPublicEndpoint, JSON.stringify(renewStatus.payload?.remoteHostTargetRenew));
470
+
471
+ const renewClear = await fetchJson(`${renewHost.baseUrl}/api/remote/host-target`, {
472
+ method: 'DELETE',
473
+ token: BRIDGE_TOKEN,
474
+ body: JSON.stringify({
475
+ nodeId: 'remote-registry-renew-node'
476
+ })
477
+ });
478
+ assert.equal(renewClear.ok, true, JSON.stringify(renewClear.payload));
479
+ assert.equal(renewClear.payload?.ok, true, JSON.stringify(renewClear.payload));
480
+ await renewHost.stop();
481
+ renewHost = null;
482
+
483
+ currentTarget = createRegistryTarget({
484
+ endpoint: hostAEndpoint,
485
+ endpointCandidates: [staleEndpoint, hostAEndpoint],
486
+ leaseId: 'lease-a'
487
+ });
488
+
384
489
  hostA = spawnBridge({
385
490
  bridgePort: hostABridgePort,
386
491
  remoteHubPort: hostARemotePort,
@@ -465,6 +570,7 @@ async function main() {
465
570
  if (client) await client.stop();
466
571
  if (hostB) await hostB.stop();
467
572
  if (hostA) await hostA.stop();
573
+ if (renewHost) await renewHost.stop();
468
574
  if (fakeSupabase) await fakeSupabase.stop();
469
575
  await rm(tempRoot, { recursive: true, force: true });
470
576
  }
package/server.js CHANGED
@@ -3146,6 +3146,15 @@ const REMOTE_REGISTRY_REALTIME_ENABLED = REMOTE_REGISTRY_FOLLOWER_ENABLED
3146
3146
  const REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS = Math.max(10000, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS || 25000) || 25000);
3147
3147
  const REMOTE_REGISTRY_REALTIME_RECONNECT_MS = Math.max(1000, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_RECONNECT_MS || 2500) || 2500);
3148
3148
  const REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS = Math.max(100, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS || 250) || 250);
3149
+ const REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED = REMOTE_REGISTRY_FOLLOWER_ENABLED
3150
+ && !/^(0|false|no|off)$/i.test(String(process.env.MINDEXEC_REMOTE_HOST_TARGET_AUTO_RENEW || 'true'));
3151
+ const REMOTE_HOST_TARGET_LEASE_MS = Math.max(60000, Number(process.env.MINDEXEC_REMOTE_HOST_TARGET_LEASE_MS || 120000) || 120000);
3152
+ const REMOTE_HOST_TARGET_RENEW_MS = Math.max(
3153
+ 5000,
3154
+ Math.min(
3155
+ Math.floor(REMOTE_HOST_TARGET_LEASE_MS / 2),
3156
+ Number(process.env.MINDEXEC_REMOTE_HOST_TARGET_RENEW_MS || 25000) || 25000));
3157
+ const REMOTE_HOST_TARGET_RENEW_LOG_REPEAT_MS = 60000;
3149
3158
  let remoteAgentState = createRemoteAgentIdleState();
3150
3159
  let remoteAgentSyncReportState = null;
3151
3160
  let remoteAgentSyncReportLogKey = '';
@@ -3168,6 +3177,10 @@ let remoteRegistryRealtimeHeartbeatTimer = null;
3168
3177
  let remoteRegistryRealtimeReconnectTimer = null;
3169
3178
  let remoteRegistryRealtimeReconnectContext = null;
3170
3179
  let remoteRegistryRealtimeLastWakeAt = 0;
3180
+ let remoteHostTargetRenewTimer = null;
3181
+ let remoteHostTargetRenewInFlight = false;
3182
+ let remoteHostTargetRenewLogKey = '';
3183
+ let remoteHostTargetRenewLogAt = 0;
3171
3184
  let remoteRegistryFollowerState = {
3172
3185
  enabled: REMOTE_REGISTRY_FOLLOWER_ENABLED,
3173
3186
  status: REMOTE_REGISTRY_FOLLOWER_ENABLED ? 'idle' : 'disabled',
@@ -3201,6 +3214,20 @@ let remoteRegistryRealtimeState = {
3201
3214
  changes: 0,
3202
3215
  wakeups: 0
3203
3216
  };
3217
+ let remoteHostTargetRenewState = {
3218
+ enabled: REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED,
3219
+ status: REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED ? 'idle' : 'disabled',
3220
+ reason: '',
3221
+ nodeId: '',
3222
+ leaseId: '',
3223
+ hostInstanceId: '',
3224
+ endpoint: '',
3225
+ endpointCandidates: [],
3226
+ expiresAt: '',
3227
+ lastAttemptAt: '',
3228
+ lastSuccessAt: '',
3229
+ lastError: ''
3230
+ };
3204
3231
 
3205
3232
  function createRemoteAgentIdleState(overrides = {}) {
3206
3233
  return {
@@ -4499,6 +4526,23 @@ function updateRemoteRegistryFollowerState(patch = {}) {
4499
4526
  emitBridgeEvent('RemoteRegistryFollowerUpdated', serializeRemoteRegistryFollowerState());
4500
4527
  }
4501
4528
 
4529
+ function serializeRemoteHostTargetRenewState() {
4530
+ return {
4531
+ ...remoteHostTargetRenewState,
4532
+ leaseMs: REMOTE_HOST_TARGET_LEASE_MS,
4533
+ renewMs: REMOTE_HOST_TARGET_RENEW_MS
4534
+ };
4535
+ }
4536
+
4537
+ function updateRemoteHostTargetRenewState(patch = {}) {
4538
+ remoteHostTargetRenewState = {
4539
+ ...remoteHostTargetRenewState,
4540
+ ...patch,
4541
+ enabled: REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED
4542
+ };
4543
+ emitBridgeEvent('RemoteHostTargetRenewUpdated', serializeRemoteHostTargetRenewState());
4544
+ }
4545
+
4502
4546
  function serializeRemoteRegistryRealtimeState() {
4503
4547
  return {
4504
4548
  ...remoteRegistryRealtimeState,
@@ -5094,6 +5138,328 @@ async function fetchRemoteRegistryTarget(config, session) {
5094
5138
  return normalizeRemoteRegistryTarget(Array.isArray(payload) ? payload[0] : payload);
5095
5139
  }
5096
5140
 
5141
+ async function callRemoteRegistryRpc(config, session, functionName, payload) {
5142
+ const url = new URL(`/rest/v1/rpc/${functionName}`, config.url);
5143
+ const response = await fetch(url.toString(), {
5144
+ method: 'POST',
5145
+ headers: {
5146
+ apikey: config.key,
5147
+ Authorization: `Bearer ${session.accessToken}`,
5148
+ Accept: 'application/json',
5149
+ 'Content-Type': 'application/json'
5150
+ },
5151
+ body: JSON.stringify(payload)
5152
+ });
5153
+
5154
+ if (!response.ok) {
5155
+ const text = await response.text().catch(() => '');
5156
+ const error = new Error(`registry-rpc-${functionName}-${response.status}${text ? `:${shortenText(text, 160)}` : ''}`);
5157
+ error.statusCode = response.status;
5158
+ throw error;
5159
+ }
5160
+
5161
+ const rpcPayload = await response.json().catch(() => null);
5162
+ const row = Array.isArray(rpcPayload) ? rpcPayload[0] : rpcPayload;
5163
+ return {
5164
+ ok: row?.ok === true || row?.Ok === true,
5165
+ reason: safeRemoteAgentField(row?.reason || row?.Reason || '', 160)
5166
+ };
5167
+ }
5168
+
5169
+ async function readRemoteRegistryContext() {
5170
+ const config = readSupabaseRuntimeConfig();
5171
+ if (!config.url || !config.key) {
5172
+ return { ok: false, reason: 'supabase-config-missing' };
5173
+ }
5174
+
5175
+ const sessionPayload = await readStableAuthSessionPayload();
5176
+ const session = sessionPayload.found ? parseSupabaseSessionForRegistry(sessionPayload.content) : null;
5177
+ if (!session) {
5178
+ return { ok: false, reason: 'registry-not-authenticated' };
5179
+ }
5180
+
5181
+ if (session.expiresAtMs && session.expiresAtMs <= Date.now()) {
5182
+ return { ok: false, reason: 'session-expired' };
5183
+ }
5184
+
5185
+ return { ok: true, config, session };
5186
+ }
5187
+
5188
+ function getRemoteHostTargetEndpointReason(endpoint) {
5189
+ const normalized = normalizeRemoteManagerEndpoint(endpoint);
5190
+ if (!normalized) {
5191
+ return 'invalid-manager-endpoint';
5192
+ }
5193
+
5194
+ const match = normalized.match(/^(\[[^\]]+\]|[^:\s]+):(\d{1,5})$/);
5195
+ const host = String(match?.[1] || '').replace(/^\[|\]$/g, '').toLowerCase();
5196
+ if (!host || host === 'localhost' || host === '::1' || host === '0:0:0:0:0:0:0:1' || /^127\./.test(host)) {
5197
+ return 'loopback-endpoint';
5198
+ }
5199
+
5200
+ if (host === '0.0.0.0' || host === '::' || host === '*') {
5201
+ return 'wildcard-endpoint';
5202
+ }
5203
+
5204
+ if (/^169\.254\./.test(host)) {
5205
+ return 'link-local-endpoint';
5206
+ }
5207
+
5208
+ return 'ok';
5209
+ }
5210
+
5211
+ function buildRemoteHostTargetRegistryPayload(hub) {
5212
+ const rawEndpointCandidates = normalizeRemoteManagerEndpointList(
5213
+ hub?.hostTargetEndpointCandidates,
5214
+ hub?.agentEndpointCandidates,
5215
+ hub?.hostTargetEndpoint,
5216
+ hub?.agentEndpoint);
5217
+ const endpointCandidates = rawEndpointCandidates
5218
+ .filter(endpoint => getRemoteHostTargetEndpointReason(endpoint) === 'ok')
5219
+ .slice(0, 12);
5220
+ const endpoint = endpointCandidates[0] || '';
5221
+ const pairToken = safeRemoteAgentField(hub?.pairToken, 512);
5222
+ const leaseId = safeRemoteAgentField(hub?.hostTargetLeaseId, 128);
5223
+ const hostInstanceId = safeRemoteAgentField(hub?.hostTargetHostInstanceId || hub?.hostInstanceId, 128);
5224
+ const nodeId = safeRemoteAgentField(hub?.hostTargetNodeId, 128);
5225
+ const activatedAt = safeRemoteAgentField(hub?.hostTargetActivatedAt, 80) || new Date().toISOString();
5226
+ const expiresAt = safeRemoteAgentField(hub?.hostTargetExpiresAt, 80) || new Date(Date.now() + REMOTE_HOST_TARGET_LEASE_MS).toISOString();
5227
+
5228
+ if (!pairToken || !leaseId || !hostInstanceId || !nodeId) {
5229
+ return {
5230
+ ok: false,
5231
+ reason: 'missing-fields',
5232
+ endpoint: '',
5233
+ endpointCandidates: []
5234
+ };
5235
+ }
5236
+
5237
+ if (endpointCandidates.length === 0) {
5238
+ const reason = rawEndpointCandidates.length === 0
5239
+ ? safeRemoteAgentField(hub?.agentEndpointRouteReason || 'missing-endpoint-candidates', 160)
5240
+ : getRemoteHostTargetEndpointReason(rawEndpointCandidates[0]);
5241
+ return {
5242
+ ok: false,
5243
+ reason,
5244
+ endpoint: rawEndpointCandidates[0] || '',
5245
+ endpointCandidates: rawEndpointCandidates
5246
+ };
5247
+ }
5248
+
5249
+ return {
5250
+ ok: true,
5251
+ endpoint,
5252
+ endpointCandidates,
5253
+ leaseId,
5254
+ hostInstanceId,
5255
+ nodeId,
5256
+ expiresAt,
5257
+ payload: {
5258
+ p_node_id: nodeId,
5259
+ p_lease_id: leaseId,
5260
+ p_host_instance_id: hostInstanceId,
5261
+ p_endpoint: endpoint,
5262
+ p_pair_token: pairToken,
5263
+ p_manager_package: safeRemoteAgentField(hub?.managerPackage || '@mindexec/cli', 160),
5264
+ p_manager_version: safeRemoteAgentField(hub?.managerVersion || '', 80),
5265
+ p_agent_package: safeRemoteAgentField(hub?.agentPackage || '@mindexec/remote', 160),
5266
+ p_activated_at: activatedAt,
5267
+ p_expires_at: expiresAt,
5268
+ p_endpoint_candidates: endpointCandidates
5269
+ }
5270
+ };
5271
+ }
5272
+
5273
+ function logRemoteHostTargetRenew(status, reason, endpoint = '') {
5274
+ const key = [status, reason, endpoint].join('|');
5275
+ const now = Date.now();
5276
+ if (key === remoteHostTargetRenewLogKey
5277
+ && now - remoteHostTargetRenewLogAt < REMOTE_HOST_TARGET_RENEW_LOG_REPEAT_MS) {
5278
+ return;
5279
+ }
5280
+
5281
+ remoteHostTargetRenewLogKey = key;
5282
+ remoteHostTargetRenewLogAt = now;
5283
+ logEvent(
5284
+ 'remote',
5285
+ `host target renew ${status} ${formatKeyValue('reason', reason || '-')} ${formatKeyValue('endpoint', endpoint || '-')}`,
5286
+ status === 'ok' ? 'remote' : 'warn');
5287
+ }
5288
+
5289
+ function isRemoteHostTargetRenewSoftSkipReason(reason) {
5290
+ return /^(registry-not-authenticated|session-expired|supabase-config-missing|missing-endpoint-candidates|loopback-endpoint|wildcard-endpoint|link-local-endpoint)$/i
5291
+ .test(String(reason || '').trim());
5292
+ }
5293
+
5294
+ async function publishLocalRemoteHostTargetToRegistry(hub, { takeover = false } = {}) {
5295
+ const registryPayload = buildRemoteHostTargetRegistryPayload(hub);
5296
+ if (!registryPayload.ok) {
5297
+ return {
5298
+ ok: false,
5299
+ active: false,
5300
+ reason: registryPayload.reason,
5301
+ endpoint: registryPayload.endpoint,
5302
+ endpointCandidates: registryPayload.endpointCandidates,
5303
+ stale: false
5304
+ };
5305
+ }
5306
+
5307
+ const context = await readRemoteRegistryContext();
5308
+ if (!context.ok) {
5309
+ return {
5310
+ ok: false,
5311
+ active: false,
5312
+ reason: context.reason,
5313
+ endpoint: registryPayload.endpoint,
5314
+ endpointCandidates: registryPayload.endpointCandidates,
5315
+ stale: false
5316
+ };
5317
+ }
5318
+
5319
+ const row = await callRemoteRegistryRpc(
5320
+ context.config,
5321
+ context.session,
5322
+ 'set_remote_host_target',
5323
+ {
5324
+ ...registryPayload.payload,
5325
+ p_takeover: takeover === true
5326
+ });
5327
+ const reason = row.reason || (row.ok ? 'ok' : 'registry-publish-failed');
5328
+ return {
5329
+ ok: row.ok,
5330
+ active: row.ok,
5331
+ reason,
5332
+ endpoint: registryPayload.endpoint,
5333
+ endpointCandidates: registryPayload.endpointCandidates,
5334
+ leaseId: registryPayload.leaseId,
5335
+ nodeId: registryPayload.nodeId,
5336
+ hostInstanceId: registryPayload.hostInstanceId,
5337
+ expiresAt: registryPayload.expiresAt,
5338
+ stale: reason === 'host-target-taken'
5339
+ };
5340
+ }
5341
+
5342
+ function scheduleRemoteHostTargetRenew(delayMs = REMOTE_HOST_TARGET_RENEW_MS, reason = 'timer') {
5343
+ if (!REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED || isShuttingDown) {
5344
+ return;
5345
+ }
5346
+
5347
+ if (remoteHostTargetRenewTimer) {
5348
+ clearTimeout(remoteHostTargetRenewTimer);
5349
+ remoteHostTargetRenewTimer = null;
5350
+ }
5351
+
5352
+ remoteHostTargetRenewTimer = setTimeout(() => {
5353
+ remoteHostTargetRenewTimer = null;
5354
+ runRemoteHostTargetRenewOnce(reason).catch(error => {
5355
+ const message = error?.message || String(error || '');
5356
+ updateRemoteHostTargetRenewState({
5357
+ status: 'error',
5358
+ reason: 'renew-error',
5359
+ lastAttemptAt: new Date().toISOString(),
5360
+ lastError: message
5361
+ });
5362
+ logRemoteHostTargetRenew('failed', message);
5363
+ scheduleRemoteHostTargetRenew(REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS, 'error-retry');
5364
+ });
5365
+ }, Math.max(0, Number(delayMs) || 0));
5366
+ remoteHostTargetRenewTimer?.unref?.();
5367
+ }
5368
+
5369
+ function stopRemoteHostTargetRenew(reason = 'stopped') {
5370
+ if (remoteHostTargetRenewTimer) {
5371
+ clearTimeout(remoteHostTargetRenewTimer);
5372
+ remoteHostTargetRenewTimer = null;
5373
+ }
5374
+ updateRemoteHostTargetRenewState({
5375
+ status: REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED ? 'idle' : 'disabled',
5376
+ reason,
5377
+ nodeId: '',
5378
+ leaseId: '',
5379
+ hostInstanceId: '',
5380
+ endpoint: '',
5381
+ endpointCandidates: [],
5382
+ expiresAt: '',
5383
+ lastError: ''
5384
+ });
5385
+ }
5386
+
5387
+ async function runRemoteHostTargetRenewOnce(trigger = 'timer', options = {}) {
5388
+ if (!REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED || remoteHostTargetRenewInFlight) {
5389
+ return serializeRemoteHostTargetRenewState();
5390
+ }
5391
+
5392
+ remoteHostTargetRenewInFlight = true;
5393
+ const attemptedAt = new Date().toISOString();
5394
+ try {
5395
+ const currentHub = remoteHub.getStatus({ includeSecrets: true });
5396
+ if (currentHub?.hostTargetActive !== true || !currentHub.hostTargetNodeId) {
5397
+ stopRemoteHostTargetRenew('no-local-host-target');
5398
+ return serializeRemoteHostTargetRenewState();
5399
+ }
5400
+
5401
+ const renewed = remoteHub.setHostTarget({
5402
+ enabled: true,
5403
+ nodeId: currentHub.hostTargetNodeId,
5404
+ leaseMs: REMOTE_HOST_TARGET_LEASE_MS
5405
+ });
5406
+ if (renewed?.ok !== true || renewed?.active !== true) {
5407
+ updateRemoteHostTargetRenewState({
5408
+ status: 'error',
5409
+ reason: renewed?.error || 'local-host-renew-failed',
5410
+ nodeId: currentHub.hostTargetNodeId,
5411
+ lastAttemptAt: attemptedAt,
5412
+ lastError: renewed?.error || 'local-host-renew-failed'
5413
+ });
5414
+ scheduleRemoteHostTargetRenew(REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS, 'local-renew-failed');
5415
+ return serializeRemoteHostTargetRenewState();
5416
+ }
5417
+
5418
+ const hub = remoteHub.getStatus({ includeSecrets: true });
5419
+ const registry = await publishLocalRemoteHostTargetToRegistry(hub, {
5420
+ takeover: options.takeover === true
5421
+ });
5422
+ const status = registry.ok ? 'active' : registry.stale ? 'superseded' : 'skipped';
5423
+ updateRemoteHostTargetRenewState({
5424
+ status,
5425
+ reason: registry.reason || (registry.ok ? 'ok' : 'registry-skipped'),
5426
+ nodeId: hub.hostTargetNodeId,
5427
+ leaseId: hub.hostTargetLeaseId,
5428
+ hostInstanceId: hub.hostTargetHostInstanceId || hub.hostInstanceId,
5429
+ endpoint: registry.endpoint || hub.hostTargetEndpoint,
5430
+ endpointCandidates: registry.endpointCandidates || hub.hostTargetEndpointCandidates || [],
5431
+ expiresAt: hub.hostTargetExpiresAt,
5432
+ lastAttemptAt: attemptedAt,
5433
+ lastSuccessAt: registry.ok ? new Date().toISOString() : remoteHostTargetRenewState.lastSuccessAt,
5434
+ lastError: registry.ok || /^(registry-not-authenticated|session-expired|supabase-config-missing)$/i.test(registry.reason || '')
5435
+ ? ''
5436
+ : (registry.reason || 'registry-publish-failed')
5437
+ });
5438
+
5439
+ if (registry.stale) {
5440
+ remoteHub.setHostTarget({
5441
+ enabled: false,
5442
+ nodeId: hub.hostTargetNodeId
5443
+ });
5444
+ logRemoteHostTargetRenew('superseded', registry.reason, registry.endpoint);
5445
+ stopRemoteHostTargetRenew('host-target-superseded');
5446
+ wakeRemoteRegistryFollower('host-target-superseded').catch(() => {});
5447
+ return serializeRemoteHostTargetRenewState();
5448
+ }
5449
+
5450
+ logRemoteHostTargetRenew(registry.ok ? 'ok' : 'skipped', registry.reason, registry.endpoint);
5451
+ const retryDelay = registry.ok || isRemoteHostTargetRenewSoftSkipReason(registry.reason)
5452
+ ? REMOTE_HOST_TARGET_RENEW_MS
5453
+ : REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS;
5454
+ scheduleRemoteHostTargetRenew(
5455
+ retryDelay,
5456
+ registry.ok ? 'renewed' : 'renew-skipped');
5457
+ return serializeRemoteHostTargetRenewState();
5458
+ } finally {
5459
+ remoteHostTargetRenewInFlight = false;
5460
+ }
5461
+ }
5462
+
5097
5463
  async function reportRemoteRegistryFollowerSync(payload = {}) {
5098
5464
  const report = createRemoteAgentSyncReport(payload);
5099
5465
  rememberRemoteAgentSyncReport(report);
@@ -9921,6 +10287,7 @@ app.get('/api/status', async (req, res) => {
9921
10287
  remoteAgent: serializeRemoteAgentState(),
9922
10288
  remoteRegistryFollower: serializeRemoteRegistryFollowerState(),
9923
10289
  remoteRegistryRealtime: serializeRemoteRegistryRealtimeState(),
10290
+ remoteHostTargetRenew: serializeRemoteHostTargetRenewState(),
9924
10291
  shellJobsPath: '/api/shell/jobs',
9925
10292
  companyCore: {
9926
10293
  baseUrl: companyCoreBaseUrl,
@@ -9997,20 +10364,27 @@ app.post('/api/remote/frames', (req, res) => {
9997
10364
  app.post('/api/remote/host-target', async (req, res) => {
9998
10365
  res.setHeader('Cache-Control', 'no-store');
9999
10366
  try {
10367
+ const requestedLeaseMs = Number(req.body?.leaseMs);
10000
10368
  const result = remoteHub.setHostTarget({
10001
10369
  enabled: req.body?.enabled !== false,
10002
10370
  nodeId: req.body?.nodeId,
10003
- leaseMs: req.body?.leaseMs
10371
+ leaseMs: Math.max(
10372
+ REMOTE_HOST_TARGET_LEASE_MS,
10373
+ Number.isFinite(requestedLeaseMs) && requestedLeaseMs > 0 ? requestedLeaseMs : 0)
10004
10374
  });
10005
10375
  if (result?.ok === true && result?.active === true && isLocalRemoteHostTargetActive()) {
10006
10376
  if (isRemoteAgentProcessRunning()) {
10007
10377
  await stopRemoteAgentConnection('local-host-target-active');
10008
10378
  logEvent('remote', 'managed RemoteAgent held stopped while local host target is active', 'remote');
10009
10379
  }
10380
+ await runRemoteHostTargetRenewOnce('set-host', { takeover: true });
10381
+ } else if (result?.active !== true) {
10382
+ stopRemoteHostTargetRenew('host-target-inactive');
10010
10383
  }
10011
10384
  res.json({
10012
10385
  ...result,
10013
- agent: serializeRemoteAgentState()
10386
+ agent: serializeRemoteAgentState(),
10387
+ hostTargetRenew: serializeRemoteHostTargetRenewState()
10014
10388
  });
10015
10389
  } catch (err) {
10016
10390
  logError('remote', 'remote host target update failed.', err);
@@ -10018,17 +10392,25 @@ app.post('/api/remote/host-target', async (req, res) => {
10018
10392
  ok: false,
10019
10393
  active: false,
10020
10394
  error: err?.message || String(err),
10021
- agent: serializeRemoteAgentState()
10395
+ agent: serializeRemoteAgentState(),
10396
+ hostTargetRenew: serializeRemoteHostTargetRenewState()
10022
10397
  });
10023
10398
  }
10024
10399
  });
10025
10400
 
10026
10401
  app.delete('/api/remote/host-target', (req, res) => {
10027
10402
  res.setHeader('Cache-Control', 'no-store');
10028
- res.json(remoteHub.setHostTarget({
10403
+ const result = remoteHub.setHostTarget({
10029
10404
  enabled: false,
10030
10405
  nodeId: req.body?.nodeId
10031
- }));
10406
+ });
10407
+ if (result?.ok === true) {
10408
+ stopRemoteHostTargetRenew('host-target-cleared');
10409
+ }
10410
+ res.json({
10411
+ ...result,
10412
+ hostTargetRenew: serializeRemoteHostTargetRenewState()
10413
+ });
10032
10414
  });
10033
10415
 
10034
10416
  app.get('/api/remote/agent/status', (req, res) => {
@@ -11751,6 +12133,12 @@ async function shutdownBridge(signal) {
11751
12133
  // Ignore registry follower shutdown errors
11752
12134
  }
11753
12135
 
12136
+ try {
12137
+ stopRemoteHostTargetRenew('bridge-shutdown');
12138
+ } catch {
12139
+ // Ignore host-target renew shutdown errors
12140
+ }
12141
+
11754
12142
  try {
11755
12143
  closeRemoteRegistryRealtime('bridge-shutdown');
11756
12144
  } catch {
@@ -5,7 +5,7 @@
5
5
  const DEBUG = false;
6
6
  const FPS_DEBUG = false;
7
7
  const FRAME_PERF_DEBUG = false;
8
- const MINDMAP_CORE_BUILD_ID = '20260616-remote-ws-control-frames-v573';
8
+ const MINDMAP_CORE_BUILD_ID = '20260617-flow-run-hitfix-v586';
9
9
  const CanvasPhase = Object.freeze({
10
10
  Booting: 'booting',
11
11
  BoardFileLoading: 'board-file-loading',
@@ -8524,6 +8524,7 @@
8524
8524
  const AUTOMATION_RESULT_PIN_HELP_TEXT = 'Drag to connect this result to an input pin. Click to view the result.';
8525
8525
  const BUSINESS_AUTOMATION_PIN_HIT_PADDING = 8;
8526
8526
  const BUSINESS_AUTOMATION_PIN_DROP_PADDING = 24;
8527
+ const BUSINESS_AUTOMATION_RESULT_PIN_EDGE_PADDING = 18;
8527
8528
 
8528
8529
  function getAutomationPinTheme(type) {
8529
8530
  const normalized = String(type || '').trim().toLowerCase();
@@ -8580,7 +8581,7 @@
8580
8581
  width: ${compact ? '24px' : 'auto'};
8581
8582
  height: ${compact ? '24px' : '22px'};
8582
8583
  max-width: ${compact ? '24px' : '124px'};
8583
- pointer-events: ${side === 'result' ? 'none' : 'auto'};
8584
+ pointer-events: ${resultTooltipPin ? 'auto' : (side === 'result' ? 'none' : 'auto')};
8584
8585
  user-select: none;
8585
8586
  -webkit-user-select: none;
8586
8587
  white-space: nowrap;
@@ -8667,13 +8668,14 @@
8667
8668
  }
8668
8669
 
8669
8670
  const placement = String(options.placement || '').trim().toLowerCase();
8671
+ const hasInteractiveResultPin = pins.some(pin => isAutomationResultTooltipPinDefinition(pin, side));
8670
8672
  const group = document.createElement('div');
8671
8673
  group.className = `map-node-memo__automation-pins is-${side}${placement ? ` is-placement-${placement}` : ''}`;
8672
8674
  group.style.cssText = `
8673
8675
  position: absolute;
8674
8676
  display: flex;
8675
8677
  gap: ${options.compact === true ? '0' : (side === 'result' ? '8px' : '7px')};
8676
- pointer-events: ${side === 'result' ? 'none' : 'auto'};
8678
+ pointer-events: ${hasInteractiveResultPin ? 'auto' : (side === 'result' ? 'none' : 'auto')};
8677
8679
  z-index: 7;
8678
8680
  ${getAutomationPinGroupPlacementStyle(side, placement)}
8679
8681
  `;
@@ -9137,11 +9139,14 @@
9137
9139
  }
9138
9140
 
9139
9141
  function handleBusinessAutomationDocumentPointerDown(event) {
9142
+ const resultPin = findBusinessAutomationResultPinAtEvent(event, {
9143
+ geometryPadding: BUSINESS_AUTOMATION_RESULT_PIN_EDGE_PADDING
9144
+ })?.pin || null;
9140
9145
  const targetPin = event.target?.closest?.('.map-node-memo__automation-pin') || null;
9141
9146
  const stackedPin = findBusinessAutomationPinAtEvent(event, '.map-node-memo__automation-pin', {
9142
9147
  geometryPadding: BUSINESS_AUTOMATION_PIN_HIT_PADDING
9143
9148
  })?.pin || null;
9144
- const pin = targetPin || stackedPin;
9149
+ const pin = resultPin || targetPin || stackedPin;
9145
9150
  const edge = event.target?.closest?.('.mind-map-business-automation-edge-hit, .mind-map-business-automation-edge-path') || null;
9146
9151
 
9147
9152
  if (!pin && !edge) {
@@ -10140,7 +10145,17 @@
10140
10145
  return;
10141
10146
  }
10142
10147
 
10143
- const pinHit = findBusinessAutomationPinAtEvent(event, '.map-node-memo__automation-pin');
10148
+ const resultPinHit = findBusinessAutomationResultPinAtEvent(event, {
10149
+ geometryPadding: BUSINESS_AUTOMATION_RESULT_PIN_EDGE_PADDING
10150
+ });
10151
+ if (resultPinHit?.pin) {
10152
+ handleBusinessAutomationPinPointerDown(event, resultPinHit.pin);
10153
+ return;
10154
+ }
10155
+
10156
+ const pinHit = findBusinessAutomationPinAtEvent(event, '.map-node-memo__automation-pin', {
10157
+ geometryPadding: BUSINESS_AUTOMATION_PIN_HIT_PADDING
10158
+ });
10144
10159
  if (pinHit?.pin) {
10145
10160
  handleBusinessAutomationPinPointerDown(event, pinHit.pin);
10146
10161
  return;
@@ -10202,6 +10217,19 @@
10202
10217
  return;
10203
10218
  }
10204
10219
 
10220
+ const resultPinHit = findBusinessAutomationResultPinAtEvent(event, {
10221
+ geometryPadding: BUSINESS_AUTOMATION_RESULT_PIN_EDGE_PADDING
10222
+ });
10223
+ const resultPin = resultPinHit?.pin || null;
10224
+ const container = resultPin?.closest?.('.map-node-automation') || null;
10225
+ if (resultPin && container && !isBusinessAutomationTooltipDragClick(event, container)) {
10226
+ event.preventDefault();
10227
+ event.stopPropagation();
10228
+ event.stopImmediatePropagation?.();
10229
+ openBusinessAutomationTooltipFromResultPin(resultPin, event);
10230
+ return;
10231
+ }
10232
+
10205
10233
  event.preventDefault();
10206
10234
  event.stopPropagation();
10207
10235
  event.stopImmediatePropagation?.();
@@ -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=20260617-mdm-tile-scale-v585" />
11
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260617-mdm-tile-scale-v585" />
10
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260617-flow-run-hitfix-v586" />
11
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260617-flow-run-hitfix-v586" />
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 = '20260617-mdm-tile-scale-v585';
582
+ const scriptVersion = '20260617-flow-run-hitfix-v586';
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": "ZfLSWK2O",
2
+ "version": "20SB8UIp",
3
3
  "assets": [
4
4
  {
5
5
  "hash": "sha256-+CSYMcqLNTsq3VnH11jgYyOCCdxvHzL74CBmo4sCmMU=",
@@ -78,7 +78,7 @@
78
78
  "url": "_content/MindExecution.Shared/js/marked.min.js"
79
79
  },
80
80
  {
81
- "hash": "sha256-oDfBxq3aqmSU1z5MGGpE9ZeTdsBtrykHEBnygOVFJtE=",
81
+ "hash": "sha256-y/ehxOpFseX3lMFu+VGjbL4lTMmQ0kEmj5QIRldR2S4=",
82
82
  "url": "_content/MindExecution.Shared/js/mind-map-core.js"
83
83
  },
84
84
  {
@@ -86,7 +86,7 @@
86
86
  "url": "_content/MindExecution.Shared/js/mind-map-core.js.backup"
87
87
  },
88
88
  {
89
- "hash": "sha256-Ma2j27vONNBesOrw65Auhtvwr3a5NFSp7sDag7VUo38=",
89
+ "hash": "sha256-CXwos9ge49EzJij/kELHzhWSDGBSR3fbqqW7hBxARCI=",
90
90
  "url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
91
91
  },
92
92
  {
@@ -834,7 +834,7 @@
834
834
  "url": "image-manifest.json"
835
835
  },
836
836
  {
837
- "hash": "sha256-uZsIbGZb6Q24FUaQCppHT2DCnj7ae7QpkaKq6YnmLIY=",
837
+ "hash": "sha256-428jP5ecE6zg8KQQ90OJuzhM+BUgcq4ko9pBmv1rm7Q=",
838
838
  "url": "index.html"
839
839
  },
840
840
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: ZfLSWK2O */
1
+ /* Manifest version: 20SB8UIp */
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