@mindexec/cli 0.2.114 → 0.2.115

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.114",
3
+ "version": "0.2.115",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "start": "node launch-bridge.cjs",
22
22
  "dev": "node launch-bridge.cjs --watch",
23
- "test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs",
23
+ "test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs",
24
24
  "test:auth": "node scripts/auth-session-smoke.mjs",
25
25
  "test:remote": "node scripts/remote-hub-smoke.mjs",
26
26
  "test:remote:scale": "node scripts/remote-hub-scale-smoke.mjs",
package/remote-hub.js CHANGED
@@ -601,6 +601,7 @@ export function createRemoteHub(options = {}) {
601
601
  const logEvent = options.logEvent || (() => {});
602
602
  const logWarn = options.logWarn || (() => {});
603
603
  const emitEvent = options.emitEvent || (() => {});
604
+ const emitFrame = typeof options.emitFrame === 'function' ? options.emitFrame : (() => {});
604
605
 
605
606
  const enabled = isEnabledValue(env.MINDEXEC_REMOTE_HUB ?? env.REMOTE_HUB_ENABLED, true)
606
607
  && !isDisabledValue(env.REMOTE_HUB_DISABLED);
@@ -985,6 +986,14 @@ export function createRemoteHub(options = {}) {
985
986
  });
986
987
  device.latestLiveFrame = frame;
987
988
  rememberRecentFramePayload(device, 'live', frame);
989
+ emitFrame({
990
+ kind: 'live',
991
+ deviceId: device.deviceId,
992
+ frame: serializeRemoteFrame(frame, device.deviceId, 'live', { includeDataUrl: false }),
993
+ payload: frame.payload,
994
+ mimeType: frame.mimeType,
995
+ byteLength: frame.byteLength
996
+ });
988
997
  device.activeLiveStream.lastFrameAt = frame.receivedAt;
989
998
  device.activeLiveStream.lastFrameSeq = frame.frameSeq;
990
999
  device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
@@ -1780,6 +1789,14 @@ export function createRemoteHub(options = {}) {
1780
1789
  accessToken: createFrameAccessToken()
1781
1790
  };
1782
1791
  rememberRecentFramePayload(device, 'live', device.latestLiveFrame);
1792
+ emitFrame({
1793
+ kind: 'live',
1794
+ deviceId: device.deviceId,
1795
+ frame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', { includeDataUrl: false }),
1796
+ payload: device.latestLiveFrame.payload,
1797
+ mimeType: device.latestLiveFrame.mimeType,
1798
+ byteLength: device.latestLiveFrame.byteLength
1799
+ });
1783
1800
  device.activeLiveStream.lastFrameAt = device.lastSeenAt;
1784
1801
  device.activeLiveStream.lastFrameSeq = frameSeq;
1785
1802
  device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import net from 'node:net';
5
+ import { spawn } from 'node:child_process';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { WebSocket } from 'ws';
9
+
10
+ const BRIDGE_TOKEN = 'remote-frame-ws-smoke-token';
11
+ const PAIR_TOKEN = 'remote-frame-ws-pair-token';
12
+
13
+ function wait(ms) {
14
+ return new Promise(resolve => setTimeout(resolve, ms));
15
+ }
16
+
17
+ async function findFreePort() {
18
+ return await new Promise((resolve, reject) => {
19
+ const server = net.createServer();
20
+ server.unref();
21
+ server.once('error', reject);
22
+ server.listen(0, '127.0.0.1', () => {
23
+ const address = server.address();
24
+ const port = typeof address === 'object' && address ? address.port : 0;
25
+ server.close(() => resolve(port));
26
+ });
27
+ });
28
+ }
29
+
30
+ async function fetchJson(url, options = {}) {
31
+ const response = await fetch(url, {
32
+ ...options,
33
+ headers: {
34
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
35
+ 'X-Bridge-Token': BRIDGE_TOKEN,
36
+ ...(options.headers || {})
37
+ }
38
+ });
39
+ const payload = await response.json().catch(() => null);
40
+ return { ok: response.ok, status: response.status, payload };
41
+ }
42
+
43
+ async function waitForBridge(baseUrl, details) {
44
+ const startedAt = Date.now();
45
+ while (Date.now() - startedAt < 30000) {
46
+ try {
47
+ const result = await fetchJson(`${baseUrl}/api/remote/status`);
48
+ if (result.ok && result.payload?.started === true) {
49
+ return result.payload;
50
+ }
51
+ } catch {
52
+ // Server is still starting.
53
+ }
54
+ await wait(100);
55
+ }
56
+
57
+ throw new Error(`Timed out waiting for LocalBridge.\n${details()}`);
58
+ }
59
+
60
+ function spawnBridge({ bridgePort, remoteHubPort }) {
61
+ const bridgeRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
62
+ const env = {
63
+ ...process.env,
64
+ BRIDGE_PORT: String(bridgePort),
65
+ BRIDGE_TOKEN,
66
+ BRIDGE_REQUIRE_TOKEN: '1',
67
+ MINDEXEC_REMOTE_HUB: '1',
68
+ MINDEXEC_REMOTE_SYNTHETIC_FLEET: '1',
69
+ REMOTE_HUB_HOST: '127.0.0.1',
70
+ REMOTE_HUB_PORT: String(remoteHubPort),
71
+ REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
72
+ NO_COLOR: '1'
73
+ };
74
+
75
+ const child = spawn(process.execPath, ['server.js'], {
76
+ cwd: bridgeRoot,
77
+ stdio: ['ignore', 'pipe', 'pipe'],
78
+ windowsHide: true,
79
+ env
80
+ });
81
+
82
+ let stdout = '';
83
+ let stderr = '';
84
+ child.stdout.on('data', chunk => {
85
+ stdout += chunk.toString();
86
+ });
87
+ child.stderr.on('data', chunk => {
88
+ stderr += chunk.toString();
89
+ });
90
+
91
+ const exitPromise = new Promise(resolve => child.once('exit', resolve));
92
+ const stop = async () => {
93
+ if (child.exitCode === null && !child.killed) {
94
+ child.kill('SIGTERM');
95
+ await Promise.race([exitPromise, wait(5000)]);
96
+ if (child.exitCode === null && !child.killed) {
97
+ child.kill('SIGKILL');
98
+ }
99
+ }
100
+ };
101
+
102
+ return {
103
+ baseUrl: `http://127.0.0.1:${bridgePort}`,
104
+ details: () => `stdout=${stdout}\nstderr=${stderr}`,
105
+ stop
106
+ };
107
+ }
108
+
109
+ function parseFramePacket(data) {
110
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
111
+ assert.ok(buffer.length > 8, 'binary frame packet should include header, metadata, and payload');
112
+ const metaLength = buffer.readUInt32BE(0);
113
+ assert.ok(metaLength > 0 && metaLength < 64 * 1024, 'metadata length should be bounded');
114
+ assert.ok(4 + metaLength < buffer.length, 'packet should include binary payload after metadata');
115
+ const metadata = JSON.parse(buffer.subarray(4, 4 + metaLength).toString('utf8'));
116
+ const payload = buffer.subarray(4 + metaLength);
117
+ return { metadata, payload };
118
+ }
119
+
120
+ async function waitForBinaryFrame(ws) {
121
+ return await new Promise((resolve, reject) => {
122
+ const timeout = setTimeout(() => reject(new Error('timed out waiting for binary frame')), 10000);
123
+ ws.on('message', data => {
124
+ if (typeof data === 'string') {
125
+ return;
126
+ }
127
+ const raw = Buffer.isBuffer(data) ? data : Buffer.from(data);
128
+ if (raw[0] === 0x7b || raw[0] === 0x5b) {
129
+ return;
130
+ }
131
+ clearTimeout(timeout);
132
+ try {
133
+ resolve(parseFramePacket(raw));
134
+ } catch (error) {
135
+ reject(error);
136
+ }
137
+ });
138
+ ws.on('error', reject);
139
+ ws.on('close', () => reject(new Error('websocket closed before binary frame')));
140
+ });
141
+ }
142
+
143
+ async function main() {
144
+ const bridgePort = await findFreePort();
145
+ const remoteHubPort = await findFreePort();
146
+ const bridge = spawnBridge({ bridgePort, remoteHubPort });
147
+
148
+ try {
149
+ const status = await waitForBridge(bridge.baseUrl, bridge.details);
150
+ assert.equal(status.started, true);
151
+
152
+ const seed = await fetchJson(`${bridge.baseUrl}/api/remote/synthetic/seed`, {
153
+ method: 'POST',
154
+ body: JSON.stringify({ count: 4, connectedRatio: 1, liveRatio: 1 })
155
+ });
156
+ assert.equal(seed.ok, true);
157
+
158
+ const devicesResult = await fetchJson(`${bridge.baseUrl}/api/remote/devices`);
159
+ assert.equal(devicesResult.ok, true);
160
+ const device = devicesResult.payload?.devices?.find(item => item.connected === true);
161
+ assert.ok(device?.deviceId, 'expected connected synthetic device');
162
+
163
+ const ws = new WebSocket(`${bridge.baseUrl.replace(/^http:/, 'ws:')}/api/remote/frames/ws?token=${encodeURIComponent(BRIDGE_TOKEN)}`);
164
+ await new Promise((resolve, reject) => {
165
+ ws.once('open', resolve);
166
+ ws.once('error', reject);
167
+ });
168
+ ws.send(JSON.stringify({ type: 'subscribe', deviceIds: [device.deviceId] }));
169
+
170
+ const framePromise = waitForBinaryFrame(ws);
171
+ const live = await fetchJson(`${bridge.baseUrl}/api/remote/devices/${encodeURIComponent(device.deviceId)}/live/start`, {
172
+ method: 'POST',
173
+ body: JSON.stringify({ fps: 10 })
174
+ });
175
+ assert.equal(live.ok, true);
176
+
177
+ const frame = await framePromise;
178
+ assert.equal(frame.metadata.type, 'remote.frame.binary');
179
+ assert.equal(frame.metadata.kind, 'live');
180
+ assert.equal(frame.metadata.deviceId, device.deviceId);
181
+ assert.ok(frame.metadata.frameSeq > 0, 'frameSeq should be positive');
182
+ assert.equal(frame.metadata.mimeType, 'image/png');
183
+ assert.ok(frame.payload.length > 0, 'payload should be non-empty');
184
+ ws.close();
185
+
186
+ console.log('Remote frame WebSocket smoke OK');
187
+ } finally {
188
+ await bridge.stop();
189
+ }
190
+ }
191
+
192
+ main().catch(error => {
193
+ console.error(error);
194
+ process.exitCode = 1;
195
+ });
package/server.js CHANGED
@@ -2120,28 +2120,41 @@ app.get('/api/local-files/:id', async (req, res) => {
2120
2120
 
2121
2121
  const httpServer = createServer(app);
2122
2122
  const wss = new WebSocketServer({ noServer: true });
2123
+ const remoteFrameWss = new WebSocketServer({ noServer: true });
2123
2124
  const wsClients = new Set();
2124
- const shellJobs = new Map();
2125
-
2126
- httpServer.on('upgrade', (req, socket, head) => {
2127
- try {
2128
- const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
2129
- const token = parsed.searchParams.get('token') || '';
2130
-
2131
- if (parsed.pathname !== '/events' || token !== wsToken) {
2132
- socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
2133
- socket.destroy();
2134
- return;
2135
- }
2136
-
2137
- wss.handleUpgrade(req, socket, head, (ws) => {
2138
- wss.emit('connection', ws, req);
2139
- });
2140
- } catch {
2141
- socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
2142
- socket.destroy();
2143
- }
2144
- });
2125
+ const remoteFrameClients = new Set();
2126
+ const shellJobs = new Map();
2127
+
2128
+ httpServer.on('upgrade', (req, socket, head) => {
2129
+ try {
2130
+ const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
2131
+ const token = parsed.searchParams.get('token') || '';
2132
+
2133
+ if (parsed.pathname === '/events' && token === wsToken) {
2134
+ wss.handleUpgrade(req, socket, head, (ws) => {
2135
+ wss.emit('connection', ws, req);
2136
+ });
2137
+ return;
2138
+ }
2139
+
2140
+ if (parsed.pathname === '/api/remote/frames/ws'
2141
+ && (!bridgeAuthRequired || token === bridgeToken)) {
2142
+ remoteFrameWss.handleUpgrade(req, socket, head, (ws) => {
2143
+ remoteFrameWss.emit('connection', ws, req);
2144
+ });
2145
+ return;
2146
+ }
2147
+
2148
+ {
2149
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
2150
+ socket.destroy();
2151
+ return;
2152
+ }
2153
+ } catch {
2154
+ socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
2155
+ socket.destroy();
2156
+ }
2157
+ });
2145
2158
 
2146
2159
  wss.on('connection', (ws) => {
2147
2160
  wsClients.add(ws);
@@ -2154,11 +2167,168 @@ wss.on('connection', (ws) => {
2154
2167
  projectRoot: projectSession.rootPath,
2155
2168
  fileCount: projectSession.symbolsByFile.size
2156
2169
  }
2157
- }));
2158
- });
2159
-
2160
- // Helper: Validate path is within workspace
2161
- function validatePath(requestedPath) {
2170
+ }));
2171
+ });
2172
+
2173
+ function normalizeRemoteFrameWsDeviceIds(value) {
2174
+ const raw = [];
2175
+ if (Array.isArray(value)) {
2176
+ raw.push(...value);
2177
+ } else if (typeof value === 'string') {
2178
+ raw.push(...value.split(','));
2179
+ }
2180
+
2181
+ const ids = [];
2182
+ const seen = new Set();
2183
+ for (const entry of raw) {
2184
+ const id = String(entry || '').trim();
2185
+ if (!id || seen.has(id)) {
2186
+ continue;
2187
+ }
2188
+
2189
+ seen.add(id);
2190
+ ids.push(id);
2191
+ if (ids.length >= 240) {
2192
+ break;
2193
+ }
2194
+ }
2195
+
2196
+ return ids;
2197
+ }
2198
+
2199
+ function sendRemoteFrameClientJson(ws, payload) {
2200
+ if (!ws || ws.readyState !== 1) {
2201
+ return false;
2202
+ }
2203
+
2204
+ try {
2205
+ ws.send(JSON.stringify(payload));
2206
+ return true;
2207
+ } catch {
2208
+ return false;
2209
+ }
2210
+ }
2211
+
2212
+ function updateRemoteFrameClientSubscription(ws, payload = {}) {
2213
+ const deviceIds = normalizeRemoteFrameWsDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId);
2214
+ ws.remoteFrameDeviceIds = new Set(deviceIds);
2215
+ ws.remoteFrameSubscriptionUpdatedAt = new Date().toISOString();
2216
+ sendRemoteFrameClientJson(ws, {
2217
+ type: 'RemoteFrameSubscription',
2218
+ timestamp: ws.remoteFrameSubscriptionUpdatedAt,
2219
+ deviceIds,
2220
+ mode: deviceIds.length > 0 ? 'selected-devices' : 'all-devices'
2221
+ });
2222
+ }
2223
+
2224
+ function buildRemoteFrameBinaryPacket(frameEvent) {
2225
+ const payload = frameEvent?.payload;
2226
+ if (!Buffer.isBuffer(payload) || payload.length === 0) {
2227
+ return null;
2228
+ }
2229
+
2230
+ const frame = frameEvent.frame || {};
2231
+ const metadata = {
2232
+ type: 'remote.frame.binary',
2233
+ kind: frameEvent.kind || 'live',
2234
+ deviceId: frameEvent.deviceId || frame.deviceId || '',
2235
+ frameSeq: Number(frame.frameSeq || 0) || 0,
2236
+ streamId: frame.streamId || '',
2237
+ width: Number(frame.width || 0) || 0,
2238
+ height: Number(frame.height || 0) || 0,
2239
+ mimeType: frameEvent.mimeType || frame.mimeType || frame.format || 'image/jpeg',
2240
+ mode: frame.mode || '',
2241
+ fps: Number(frame.fps || 0) || 0,
2242
+ capturedAt: frame.capturedAt || '',
2243
+ receivedAt: frame.receivedAt || '',
2244
+ byteLength: Number(frameEvent.byteLength || payload.length) || payload.length,
2245
+ transport: frame.transport || '',
2246
+ contentHash: frame.contentHash || '',
2247
+ captureMs: Number(frame.captureMs || 0) || 0,
2248
+ sameContentStreak: Number(frame.sameContentStreak || 0) || 0
2249
+ };
2250
+ const metaBuffer = Buffer.from(JSON.stringify(metadata), 'utf8');
2251
+ const header = Buffer.allocUnsafe(4);
2252
+ header.writeUInt32BE(metaBuffer.length, 0);
2253
+ return Buffer.concat([header, metaBuffer, payload], header.length + metaBuffer.length + payload.length);
2254
+ }
2255
+
2256
+ function broadcastRemoteBinaryFrame(frameEvent) {
2257
+ if (remoteFrameClients.size === 0) {
2258
+ return;
2259
+ }
2260
+
2261
+ const deviceId = String(frameEvent?.deviceId || frameEvent?.frame?.deviceId || '').trim();
2262
+ if (!deviceId) {
2263
+ return;
2264
+ }
2265
+
2266
+ const packet = buildRemoteFrameBinaryPacket(frameEvent);
2267
+ if (!packet) {
2268
+ return;
2269
+ }
2270
+
2271
+ for (const client of remoteFrameClients) {
2272
+ if (client.readyState !== 1) {
2273
+ continue;
2274
+ }
2275
+
2276
+ const ids = client.remoteFrameDeviceIds;
2277
+ if (ids instanceof Set && ids.size > 0 && !ids.has(deviceId)) {
2278
+ continue;
2279
+ }
2280
+
2281
+ if (client.bufferedAmount > 8 * 1024 * 1024) {
2282
+ client.remoteFrameDroppedCount = (client.remoteFrameDroppedCount || 0) + 1;
2283
+ continue;
2284
+ }
2285
+
2286
+ try {
2287
+ client.send(packet, { binary: true });
2288
+ client.remoteFrameSentCount = (client.remoteFrameSentCount || 0) + 1;
2289
+ } catch {
2290
+ client.remoteFrameDroppedCount = (client.remoteFrameDroppedCount || 0) + 1;
2291
+ }
2292
+ }
2293
+ }
2294
+
2295
+ remoteFrameWss.on('connection', (ws, req) => {
2296
+ remoteFrameClients.add(ws);
2297
+ ws.binaryType = 'arraybuffer';
2298
+ try {
2299
+ const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
2300
+ updateRemoteFrameClientSubscription(ws, {
2301
+ deviceIds: parsed.searchParams.get('devices') || ''
2302
+ });
2303
+ } catch {
2304
+ updateRemoteFrameClientSubscription(ws, {});
2305
+ }
2306
+
2307
+ ws.on('message', data => {
2308
+ try {
2309
+ const text = Buffer.isBuffer(data) ? data.toString('utf8') : String(data || '');
2310
+ const payload = JSON.parse(text);
2311
+ if (payload?.type === 'subscribe') {
2312
+ updateRemoteFrameClientSubscription(ws, payload);
2313
+ }
2314
+ } catch {
2315
+ sendRemoteFrameClientJson(ws, {
2316
+ type: 'RemoteFrameSubscriptionError',
2317
+ error: 'invalid-message'
2318
+ });
2319
+ }
2320
+ });
2321
+ ws.on('close', () => remoteFrameClients.delete(ws));
2322
+ ws.on('error', () => remoteFrameClients.delete(ws));
2323
+ sendRemoteFrameClientJson(ws, {
2324
+ type: 'RemoteFrameSocketReady',
2325
+ timestamp: new Date().toISOString(),
2326
+ protocol: 'mindexec.remote.frames.binary.v1'
2327
+ });
2328
+ });
2329
+
2330
+ // Helper: Validate path is within workspace
2331
+ function validatePath(requestedPath) {
2162
2332
  const fullPath = resolveWorkspaceBoundPath(requestedPath);
2163
2333
  if (!isPathWithin(workspacePath, fullPath)) {
2164
2334
  throw new Error('Path must be within workspace');
@@ -2726,6 +2896,7 @@ const remoteHub = createRemoteHub({
2726
2896
  logWarn,
2727
2897
  logError,
2728
2898
  emitEvent: emitBridgeEvent,
2899
+ emitFrame: broadcastRemoteBinaryFrame,
2729
2900
  managerPackage: BRIDGE_PACKAGE_NAME,
2730
2901
  managerVersion: BRIDGE_VERSION,
2731
2902
  hostInstanceId: BRIDGE_INSTANCE_ID
@@ -7772,8 +7943,9 @@ app.get('/api/status', async (req, res) => {
7772
7943
  status: 'ok',
7773
7944
  version: BRIDGE_VERSION,
7774
7945
  workspace: workspacePath,
7775
- wsToken,
7776
- wsPath: '/events',
7946
+ wsToken,
7947
+ wsPath: '/events',
7948
+ remoteFrameWsPath: '/api/remote/frames/ws',
7777
7949
  bridgeToken,
7778
7950
  bridgeTokenHeader,
7779
7951
  bridgeAuthRequired,
@@ -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-agent-sticky-overlay-frames-v571';
8
+ const MINDMAP_CORE_BUILD_ID = '20260616-remote-ws-binary-frames-v572';
9
9
  const CanvasPhase = Object.freeze({
10
10
  Booting: 'booting',
11
11
  BoardFileLoading: 'board-file-loading',
@@ -13351,13 +13351,17 @@
13351
13351
  const REMOTE_FLEET_FRAME_BLOB_CACHE_LIMIT = 96;
13352
13352
  const REMOTE_FLEET_LIVE_FRAME_DECODE_TIMEOUT_MS = 180;
13353
13353
  const REMOTE_FLEET_THUMBNAIL_FRAME_DECODE_TIMEOUT_MS = 1200;
13354
+ const REMOTE_FLEET_BINARY_FRAME_WS_RECONNECT_MS = 1500;
13355
+ const REMOTE_FLEET_BINARY_FRAME_SUBSCRIBE_MS = 1000;
13354
13356
  const REMOTE_FLEET_TASK_FOLLOW_INITIAL_MS = 250;
13355
13357
  const REMOTE_FLEET_TASK_FOLLOW_REFRESH_MS = 2000;
13356
13358
  const REMOTE_FLEET_TASK_FOLLOW_MAX_TICKS = 60;
13357
13359
  const REMOTE_FLEET_HOST_LEASE_REFRESH_MS = 10000;
13358
13360
  const remoteFleetHostLeaseTimers = new Map();
13359
13361
  const remoteFleetLocalHostTargets = new Map();
13362
+ const remoteFleetBinaryFrameSessions = new Map();
13360
13363
  const remoteFleetFrameBlobCache = new Map();
13364
+ let remoteFleetBridgeStatusPromise = null;
13361
13365
  let activeRemoteFleetControlPopup = null;
13362
13366
 
13363
13367
  function findRemoteFleetBodyByNodeId(nodeId) {
@@ -13495,6 +13499,7 @@
13495
13499
 
13496
13500
  function clearRemoteFleetTimers(bodyView) {
13497
13501
  if (!bodyView) return;
13502
+ releaseRemoteFleetBinaryFrameSocket(bodyView);
13498
13503
  if (bodyView._remoteFleetLiveRefreshTimer) {
13499
13504
  clearInterval(bodyView._remoteFleetLiveRefreshTimer);
13500
13505
  bodyView._remoteFleetLiveRefreshTimer = null;
@@ -13519,6 +13524,323 @@
13519
13524
  }
13520
13525
  }
13521
13526
 
13527
+ function getRemoteFleetBridgeBaseUrlCandidates() {
13528
+ const candidates = [];
13529
+ const push = value => {
13530
+ const text = String(value || '').trim().replace(/\/+$/, '');
13531
+ if (text && !candidates.includes(text)) {
13532
+ candidates.push(text);
13533
+ }
13534
+ };
13535
+
13536
+ push(window.MindExecLocalBridge?.activeBridgeUrl);
13537
+ push(window.location?.origin);
13538
+ return candidates;
13539
+ }
13540
+
13541
+ async function getRemoteFleetBridgeStatusForFrames() {
13542
+ if (remoteFleetBridgeStatusPromise) {
13543
+ return remoteFleetBridgeStatusPromise;
13544
+ }
13545
+
13546
+ remoteFleetBridgeStatusPromise = (async () => {
13547
+ const nonce = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
13548
+ let lastError = null;
13549
+ for (const baseUrl of getRemoteFleetBridgeBaseUrlCandidates()) {
13550
+ try {
13551
+ const response = await fetch(`${baseUrl}/api/status?remoteFrames=ws&cb=${encodeURIComponent(nonce)}`, {
13552
+ cache: 'no-store',
13553
+ credentials: 'omit'
13554
+ });
13555
+ if (!response.ok) {
13556
+ lastError = new Error(`status:${response.status}`);
13557
+ continue;
13558
+ }
13559
+
13560
+ const status = await response.json();
13561
+ status.__remoteFleetBridgeBaseUrl = baseUrl;
13562
+ return status;
13563
+ } catch (error) {
13564
+ lastError = error;
13565
+ }
13566
+ }
13567
+
13568
+ throw lastError || new Error('bridge-status-unavailable');
13569
+ })();
13570
+
13571
+ try {
13572
+ return await remoteFleetBridgeStatusPromise;
13573
+ } catch (error) {
13574
+ remoteFleetBridgeStatusPromise = null;
13575
+ throw error;
13576
+ }
13577
+ }
13578
+
13579
+ function buildRemoteFleetBinaryFrameWsUrl(status) {
13580
+ const baseUrl = String(status?.__remoteFleetBridgeBaseUrl || window.location?.origin || '').trim();
13581
+ const path = String(status?.remoteFrameWsPath || '/api/remote/frames/ws').trim() || '/api/remote/frames/ws';
13582
+ if (!baseUrl) {
13583
+ return '';
13584
+ }
13585
+
13586
+ const url = new URL(path, baseUrl);
13587
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
13588
+ const bridgeToken = String(status?.bridgeToken || '').trim();
13589
+ if (bridgeToken) {
13590
+ url.searchParams.set('token', bridgeToken);
13591
+ }
13592
+ return url.toString();
13593
+ }
13594
+
13595
+ async function parseRemoteFleetBinaryFrameMessage(data) {
13596
+ let buffer = null;
13597
+ if (data instanceof ArrayBuffer) {
13598
+ buffer = data;
13599
+ } else if (data?.arrayBuffer && typeof data.arrayBuffer === 'function') {
13600
+ buffer = await data.arrayBuffer();
13601
+ }
13602
+
13603
+ if (!buffer || buffer.byteLength < 5) {
13604
+ return null;
13605
+ }
13606
+
13607
+ const view = new DataView(buffer);
13608
+ const metaLength = view.getUint32(0);
13609
+ if (!Number.isFinite(metaLength)
13610
+ || metaLength <= 0
13611
+ || metaLength > 64 * 1024
13612
+ || 4 + metaLength >= buffer.byteLength) {
13613
+ return null;
13614
+ }
13615
+
13616
+ const metadataBytes = new Uint8Array(buffer, 4, metaLength);
13617
+ const metadataText = new TextDecoder().decode(metadataBytes);
13618
+ const metadata = JSON.parse(metadataText);
13619
+ if (String(metadata?.type || '').toLowerCase() !== 'remote.frame.binary') {
13620
+ return null;
13621
+ }
13622
+
13623
+ const payload = buffer.slice(4 + metaLength);
13624
+ const mimeType = String(metadata.mimeType || metadata.format || 'image/jpeg').trim() || 'image/jpeg';
13625
+ const objectUrl = URL.createObjectURL(new Blob([payload], { type: mimeType }));
13626
+ return {
13627
+ ...metadata,
13628
+ kind: String(metadata.kind || 'live').toLowerCase() === 'thumbnail' ? 'thumbnail' : 'live',
13629
+ mimeType,
13630
+ frameUrl: objectUrl,
13631
+ framePath: objectUrl,
13632
+ dataUrl: '',
13633
+ _remoteFleetObjectUrl: objectUrl,
13634
+ _remoteFleetBinaryFrame: true
13635
+ };
13636
+ }
13637
+
13638
+ function releaseRemoteFleetFrameObjectUrl(preview, nextObjectUrl = '') {
13639
+ const previous = preview?._remoteFleetObjectUrl || '';
13640
+ if (previous && previous !== nextObjectUrl && typeof URL !== 'undefined' && typeof URL.revokeObjectURL === 'function') {
13641
+ try {
13642
+ URL.revokeObjectURL(previous);
13643
+ } catch {
13644
+ // Best-effort browser memory cleanup.
13645
+ }
13646
+ }
13647
+
13648
+ if (preview) {
13649
+ preview._remoteFleetObjectUrl = nextObjectUrl || '';
13650
+ }
13651
+ }
13652
+
13653
+ function getRemoteFleetBinaryFrameDeviceIds(getDeviceIds) {
13654
+ const ids = typeof getDeviceIds === 'function'
13655
+ ? getDeviceIds()
13656
+ : [];
13657
+ return Array.from(new Set((Array.isArray(ids) ? ids : [])
13658
+ .map(value => String(value || '').trim())
13659
+ .filter(Boolean)))
13660
+ .slice(0, 240);
13661
+ }
13662
+
13663
+ function refreshRemoteFleetBinaryFrameSubscription(session, force = false) {
13664
+ if (!session?.ws || session.ws.readyState !== WebSocket.OPEN) {
13665
+ return false;
13666
+ }
13667
+
13668
+ const deviceIds = getRemoteFleetBinaryFrameDeviceIds(session.getDeviceIds);
13669
+ const key = deviceIds.join('\n');
13670
+ if (!force && session.subscriptionKey === key) {
13671
+ return true;
13672
+ }
13673
+
13674
+ session.subscriptionKey = key;
13675
+ session.ws.send(JSON.stringify({
13676
+ type: 'subscribe',
13677
+ nodeId: session.nodeId,
13678
+ deviceIds
13679
+ }));
13680
+ return true;
13681
+ }
13682
+
13683
+ function scheduleRemoteFleetBinaryFrameReconnect(session) {
13684
+ if (!session || session.reconnectTimer || session.bodies.size === 0) {
13685
+ return;
13686
+ }
13687
+
13688
+ session.reconnectTimer = setTimeout(() => {
13689
+ session.reconnectTimer = null;
13690
+ openRemoteFleetBinaryFrameSocket(session);
13691
+ }, REMOTE_FLEET_BINARY_FRAME_WS_RECONNECT_MS);
13692
+ }
13693
+
13694
+ async function openRemoteFleetBinaryFrameSocket(session) {
13695
+ if (!session || session.bodies.size === 0 || typeof WebSocket !== 'function') {
13696
+ return false;
13697
+ }
13698
+
13699
+ if (session.ws && (session.ws.readyState === WebSocket.OPEN || session.ws.readyState === WebSocket.CONNECTING)) {
13700
+ refreshRemoteFleetBinaryFrameSubscription(session);
13701
+ return true;
13702
+ }
13703
+
13704
+ try {
13705
+ const status = await getRemoteFleetBridgeStatusForFrames();
13706
+ const wsUrl = buildRemoteFleetBinaryFrameWsUrl(status);
13707
+ if (!wsUrl) {
13708
+ throw new Error('remote-frame-ws-url-unavailable');
13709
+ }
13710
+
13711
+ const ws = new WebSocket(wsUrl);
13712
+ session.ws = ws;
13713
+ session.subscriptionKey = '';
13714
+ ws.binaryType = 'arraybuffer';
13715
+ ws.onopen = () => {
13716
+ window.RuntimeTrace?.emit?.('remote.frame.wsOpen', { nodeId: session.nodeId });
13717
+ refreshRemoteFleetBinaryFrameSubscription(session, true);
13718
+ if (!session.subscriptionTimer) {
13719
+ session.subscriptionTimer = setInterval(
13720
+ () => refreshRemoteFleetBinaryFrameSubscription(session),
13721
+ REMOTE_FLEET_BINARY_FRAME_SUBSCRIBE_MS
13722
+ );
13723
+ }
13724
+ };
13725
+ ws.onmessage = event => {
13726
+ if (typeof event.data === 'string') {
13727
+ window.RuntimeTrace?.emit?.('remote.frame.wsMeta', {
13728
+ nodeId: session.nodeId,
13729
+ message: event.data.slice(0, 120)
13730
+ });
13731
+ return;
13732
+ }
13733
+
13734
+ parseRemoteFleetBinaryFrameMessage(event.data)
13735
+ .then(frame => {
13736
+ if (!frame || session.bodies.size === 0) {
13737
+ if (frame?._remoteFleetObjectUrl && typeof URL !== 'undefined' && typeof URL.revokeObjectURL === 'function') {
13738
+ URL.revokeObjectURL(frame._remoteFleetObjectUrl);
13739
+ }
13740
+ return;
13741
+ }
13742
+
13743
+ let applied = 0;
13744
+ session.bodies.forEach(body => {
13745
+ if (document.body.contains(body)) {
13746
+ applied += applyRemoteFleetFramePatches(body, [frame]);
13747
+ }
13748
+ });
13749
+
13750
+ if (applied <= 0 && frame._remoteFleetObjectUrl && typeof URL !== 'undefined' && typeof URL.revokeObjectURL === 'function') {
13751
+ URL.revokeObjectURL(frame._remoteFleetObjectUrl);
13752
+ }
13753
+ })
13754
+ .catch(error => {
13755
+ window.RuntimeTrace?.emit?.('remote.frame.wsParseFailed', {
13756
+ nodeId: session.nodeId,
13757
+ error: error?.message || String(error || '')
13758
+ });
13759
+ });
13760
+ };
13761
+ ws.onclose = () => {
13762
+ if (session.ws === ws) {
13763
+ session.ws = null;
13764
+ }
13765
+ window.RuntimeTrace?.emit?.('remote.frame.wsClosed', { nodeId: session.nodeId });
13766
+ scheduleRemoteFleetBinaryFrameReconnect(session);
13767
+ };
13768
+ ws.onerror = () => {
13769
+ try {
13770
+ ws.close();
13771
+ } catch {
13772
+ // Ignore WebSocket close failures.
13773
+ }
13774
+ };
13775
+ return true;
13776
+ } catch (error) {
13777
+ window.RuntimeTrace?.emit?.('remote.frame.wsOpenFailed', {
13778
+ nodeId: session.nodeId,
13779
+ error: error?.message || String(error || '')
13780
+ });
13781
+ scheduleRemoteFleetBinaryFrameReconnect(session);
13782
+ return false;
13783
+ }
13784
+ }
13785
+
13786
+ function startRemoteFleetBinaryFrameSocket(bodyView, getDeviceIds) {
13787
+ const nodeId = String(bodyView?.dataset?.nodeId || '').trim();
13788
+ if (!nodeId || typeof WebSocket !== 'function') {
13789
+ return false;
13790
+ }
13791
+
13792
+ let session = remoteFleetBinaryFrameSessions.get(nodeId);
13793
+ if (!session) {
13794
+ session = {
13795
+ nodeId,
13796
+ bodies: new Set(),
13797
+ ws: null,
13798
+ reconnectTimer: null,
13799
+ subscriptionTimer: null,
13800
+ subscriptionKey: '',
13801
+ getDeviceIds: null
13802
+ };
13803
+ remoteFleetBinaryFrameSessions.set(nodeId, session);
13804
+ }
13805
+
13806
+ session.bodies.add(bodyView);
13807
+ session.getDeviceIds = getDeviceIds;
13808
+ bodyView._remoteFleetBinaryFrameSession = session;
13809
+ openRemoteFleetBinaryFrameSocket(session).catch(() => undefined);
13810
+ return true;
13811
+ }
13812
+
13813
+ function releaseRemoteFleetBinaryFrameSocket(bodyView) {
13814
+ const session = bodyView?._remoteFleetBinaryFrameSession;
13815
+ if (!session) {
13816
+ return;
13817
+ }
13818
+
13819
+ delete bodyView._remoteFleetBinaryFrameSession;
13820
+ session.bodies.delete(bodyView);
13821
+ if (session.bodies.size > 0) {
13822
+ return;
13823
+ }
13824
+
13825
+ if (session.reconnectTimer) {
13826
+ clearTimeout(session.reconnectTimer);
13827
+ session.reconnectTimer = null;
13828
+ }
13829
+ if (session.subscriptionTimer) {
13830
+ clearInterval(session.subscriptionTimer);
13831
+ session.subscriptionTimer = null;
13832
+ }
13833
+ if (session.ws) {
13834
+ try {
13835
+ session.ws.close();
13836
+ } catch {
13837
+ // Ignore WebSocket close failures.
13838
+ }
13839
+ session.ws = null;
13840
+ }
13841
+ remoteFleetBinaryFrameSessions.delete(session.nodeId);
13842
+ }
13843
+
13522
13844
  function requestRemoteFleetFrameLoopFrame(callback) {
13523
13845
  if (typeof requestAnimationFrame === 'function') {
13524
13846
  return {
@@ -14021,6 +14343,7 @@
14021
14343
  }
14022
14344
 
14023
14345
  const nextSeq = Number(frame.frameSeq || 0);
14346
+ releaseRemoteFleetFrameObjectUrl(preview, frame._remoteFleetObjectUrl || '');
14024
14347
  preview.dataset.remoteFleetFrameKind = frame.kind;
14025
14348
  preview.dataset.remoteFleetFrameSeq = String(nextSeq || 0);
14026
14349
  preview.dataset.remoteFleetFrameUrl = frame.frameUrl;
@@ -17086,7 +17409,13 @@
17086
17409
  error: error?.message || String(error || '')
17087
17410
  });
17088
17411
  });
17089
- startRemoteFleetFrameLoop(bodyView, REMOTE_FLEET_LIVE_FRAME_REFRESH_MS, () => refreshRemoteFleetVisibleLiveFrames());
17412
+ const binaryFrameSocketStarted = startRemoteFleetBinaryFrameSocket(bodyView, () => {
17413
+ const liveIds = getVisibleLiveFrameDeviceIds();
17414
+ return liveIds.length > 0 ? liveIds : getVisibleFrameDeviceIds();
17415
+ });
17416
+ if (!binaryFrameSocketStarted) {
17417
+ startRemoteFleetFrameLoop(bodyView, REMOTE_FLEET_LIVE_FRAME_REFRESH_MS, () => refreshRemoteFleetVisibleLiveFrames());
17418
+ }
17090
17419
  bodyView._remoteFleetLiveRefreshTimer = setInterval(async () => {
17091
17420
  if (!document.body.contains(bodyView)) {
17092
17421
  clearRemoteFleetTimers(bodyView);
@@ -7,8 +7,8 @@
7
7
  <title>MindExec | Run your ideas as AI task graphs</title>
8
8
  <meta name="description" content="MindExec is an AI execution canvas for solo builders, researchers, developers, and creators. Start with free browser tools, then move serious work into saved MindCanvas projects." />
9
9
  <base href="/" />
10
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260616-remote-agent-sticky-overlay-frames-v571" />
11
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260616-remote-agent-sticky-overlay-frames-v571" />
10
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260616-remote-ws-binary-frames-v572" />
11
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260616-remote-ws-binary-frames-v572" />
12
12
  <!-- ?쇄뼹??Font Awesome (local) ?쇄뼹??-->
13
13
  <link rel="stylesheet" href="_content/MindExecution.Shared/lib/font-awesome/css/all.min.css" />
14
14
  <!-- ?꿎뼯??-->
@@ -579,7 +579,7 @@
579
579
  }
580
580
 
581
581
  const base = '_content/MindExecution.Shared/js/';
582
- const scriptVersion = '20260616-remote-agent-sticky-overlay-frames-v571';
582
+ const scriptVersion = '20260616-remote-ws-binary-frames-v572';
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": "WOO5ml3v",
2
+ "version": "DOWuXy9Z",
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-UtXpHLTnYeP2BPz7IAicclCMMdLjzYd29I+u5rMi270=",
81
+ "hash": "sha256-HZDEcs9fkSz0iNXD6WOKZwOj8VrjJD+r4oRH9XBQhjA=",
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-itUEN+HfFS2lC/aHq02RNF70gDWhggsJok0F3uOLHT0=",
89
+ "hash": "sha256-KgIwiH8bPKxBAXnomKCDPKRgD/hL6CSeV2nM5EpyMdQ=",
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-fojDZj+Dr+k8SI1esa0xdrCRBfXVHxr+cLNb3bvD7t8=",
837
+ "hash": "sha256-ANyJXyCBiGthYmrg5ygLEqNR6Vuu+eXOi+fQ/da00pM=",
838
838
  "url": "index.html"
839
839
  },
840
840
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: WOO5ml3v */
1
+ /* Manifest version: DOWuXy9Z */
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