@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 +86 -3
- package/package.json +1 -1
- package/scripts/remote-registry-follower-smoke.mjs +115 -9
- package/server.js +393 -5
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +1 -1
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +32 -4
- package/wwwroot/index.html +3 -3
- package/wwwroot/service-worker-assets.js +4 -4
- package/wwwroot/service-worker.js +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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 =
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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 = '
|
|
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
|
|
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?.();
|
package/wwwroot/index.html
CHANGED
|
@@ -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-
|
|
11
|
-
<link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260617-
|
|
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-
|
|
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": "
|
|
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-
|
|
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-
|
|
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-
|
|
837
|
+
"hash": "sha256-428jP5ecE6zg8KQQ90OJuzhM+BUgcq4ko9pBmv1rm7Q=",
|
|
838
838
|
"url": "index.html"
|
|
839
839
|
},
|
|
840
840
|
{
|