@mindexec/cli 0.2.10 → 0.2.11
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 +3 -2
- package/remote-hub.js +429 -15
- package/scripts/remote-hub-scale-smoke.mjs +109 -0
- package/server.js +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindexec/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "MindExec local runtime and bridge CLI",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -20,8 +20,9 @@
|
|
|
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/remote-hub-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/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs",
|
|
24
24
|
"test:remote": "node scripts/remote-hub-smoke.mjs",
|
|
25
|
+
"test:remote:scale": "node scripts/remote-hub-scale-smoke.mjs",
|
|
25
26
|
"pack:dry": "npm pack --dry-run",
|
|
26
27
|
"setup:grammars": "node scripts/setup-tree-sitter-grammars.mjs",
|
|
27
28
|
"postinstall": "npm run setup:grammars"
|
package/remote-hub.js
CHANGED
|
@@ -12,6 +12,8 @@ const MAX_AGENT_TASK_CHARS = 4000;
|
|
|
12
12
|
const MAX_AGENT_TASK_RESULT_CHARS = 3000;
|
|
13
13
|
const RECENT_TASK_LIMIT = 12;
|
|
14
14
|
const REMOTE_PROTOCOL_VERSION = 1;
|
|
15
|
+
const MAX_SYNTHETIC_DEVICES = 1000;
|
|
16
|
+
const SYNTHETIC_FRAME_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC';
|
|
15
17
|
|
|
16
18
|
function isEnabledValue(value, fallback = true) {
|
|
17
19
|
if (value === undefined || value === null || value === '') {
|
|
@@ -62,6 +64,25 @@ function readCapabilityFlag(capabilities, key) {
|
|
|
62
64
|
return /^(1|true|yes|on)$/i.test(String(value || '').trim());
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
function createDeviceCounters(overrides = {}) {
|
|
68
|
+
return {
|
|
69
|
+
messagesReceived: 0,
|
|
70
|
+
statusReceived: 0,
|
|
71
|
+
commandsSent: 0,
|
|
72
|
+
commandResultsReceived: 0,
|
|
73
|
+
thumbnailFramesReceived: 0,
|
|
74
|
+
thumbnailFramesDropped: 0,
|
|
75
|
+
liveFramesReceived: 0,
|
|
76
|
+
liveFramesDropped: 0,
|
|
77
|
+
liveStreamsStarted: 0,
|
|
78
|
+
liveStreamsStopped: 0,
|
|
79
|
+
tasksQueued: 0,
|
|
80
|
+
taskResultsReceived: 0,
|
|
81
|
+
taskResultsFailed: 0,
|
|
82
|
+
...overrides
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
function normalizeDeviceId(value) {
|
|
66
87
|
const id = safeString(value, 128).replace(/[^a-zA-Z0-9_.:-]/g, '-');
|
|
67
88
|
return id || crypto.randomUUID();
|
|
@@ -122,6 +143,7 @@ function serializeDevice(device) {
|
|
|
122
143
|
latestLiveFrame: device.latestLiveFrame ? { ...device.latestLiveFrame } : null,
|
|
123
144
|
activeLiveStream: device.activeLiveStream ? { ...device.activeLiveStream } : null,
|
|
124
145
|
latestTask: device.latestTask ? { ...device.latestTask } : null,
|
|
146
|
+
synthetic: device.synthetic === true,
|
|
125
147
|
recentTasks: Array.isArray(device.recentTasks)
|
|
126
148
|
? device.recentTasks.map(task => ({ ...task }))
|
|
127
149
|
: [],
|
|
@@ -207,6 +229,77 @@ export function createRemoteHub(options = {}) {
|
|
|
207
229
|
});
|
|
208
230
|
}
|
|
209
231
|
|
|
232
|
+
function makeSyntheticFrame(device, streamId, mode = 'thumbnail', options = {}) {
|
|
233
|
+
const now = new Date().toISOString();
|
|
234
|
+
const frameSeq = Number(device?.counters?.thumbnailFramesReceived || 0)
|
|
235
|
+
+ Number(device?.counters?.liveFramesReceived || 0)
|
|
236
|
+
+ 1;
|
|
237
|
+
return {
|
|
238
|
+
streamId: safeString(streamId, 128) || `${mode}-${Date.now()}`,
|
|
239
|
+
frameSeq,
|
|
240
|
+
commandId: safeString(options.commandId, 128),
|
|
241
|
+
width: clampNumber(options.width, 2, 2560, mode === 'remote-fast' ? 960 : 360),
|
|
242
|
+
height: clampNumber(options.height, 1, 1440, mode === 'remote-fast' ? 540 : 220),
|
|
243
|
+
mimeType: 'image/png',
|
|
244
|
+
format: 'image/png',
|
|
245
|
+
mode,
|
|
246
|
+
fps: clampNumber(options.fps, 1, 24, mode === 'remote-fast' ? 12 : 1),
|
|
247
|
+
capturedAt: now,
|
|
248
|
+
receivedAt: now,
|
|
249
|
+
byteLength: 68,
|
|
250
|
+
dataUrl: SYNTHETIC_FRAME_DATA_URL
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function applySyntheticThumbnail(device, command) {
|
|
255
|
+
if (!device) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const payload = command?.payload || {};
|
|
260
|
+
const frame = makeSyntheticFrame(device, payload.streamId || 'synthetic-thumb', 'thumbnail', {
|
|
261
|
+
commandId: command?.commandId,
|
|
262
|
+
width: payload.maxWidth || 360,
|
|
263
|
+
height: payload.maxHeight || 220
|
|
264
|
+
});
|
|
265
|
+
delete frame.mode;
|
|
266
|
+
delete frame.fps;
|
|
267
|
+
device.latestThumbnail = frame;
|
|
268
|
+
device.lastSeenAt = frame.receivedAt;
|
|
269
|
+
device.counters.thumbnailFramesReceived += 1;
|
|
270
|
+
emitRemoteEvent('RemoteFrameReceived', device, {
|
|
271
|
+
streamId: frame.streamId,
|
|
272
|
+
frameSeq: frame.frameSeq,
|
|
273
|
+
synthetic: true
|
|
274
|
+
});
|
|
275
|
+
return frame;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function applySyntheticLiveFrame(device, streamId, options = {}) {
|
|
279
|
+
if (!device?.activeLiveStream?.active) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const frame = makeSyntheticFrame(device, streamId || device.activeLiveStream.streamId, 'remote-fast', {
|
|
284
|
+
commandId: options.commandId || device.activeLiveStream.commandId,
|
|
285
|
+
width: options.maxWidth || 960,
|
|
286
|
+
height: options.maxHeight || 540,
|
|
287
|
+
fps: options.fps || device.activeLiveStream.fps
|
|
288
|
+
});
|
|
289
|
+
device.latestLiveFrame = frame;
|
|
290
|
+
device.activeLiveStream.lastFrameAt = frame.receivedAt;
|
|
291
|
+
device.activeLiveStream.lastFrameSeq = frame.frameSeq;
|
|
292
|
+
device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
|
|
293
|
+
device.lastSeenAt = frame.receivedAt;
|
|
294
|
+
device.counters.liveFramesReceived += 1;
|
|
295
|
+
emitRemoteEvent('RemoteFrameReceived', device, {
|
|
296
|
+
streamId: frame.streamId,
|
|
297
|
+
frameSeq: frame.frameSeq,
|
|
298
|
+
synthetic: true
|
|
299
|
+
});
|
|
300
|
+
return frame;
|
|
301
|
+
}
|
|
302
|
+
|
|
210
303
|
function closeExistingDeviceSocket(deviceId, nextSessionId) {
|
|
211
304
|
const existing = devices.get(deviceId);
|
|
212
305
|
if (!existing?.socket || existing.socket.destroyed) {
|
|
@@ -222,6 +315,186 @@ export function createRemoteHub(options = {}) {
|
|
|
222
315
|
existing.socket.destroy();
|
|
223
316
|
}
|
|
224
317
|
|
|
318
|
+
function clearSyntheticFleet() {
|
|
319
|
+
let removed = 0;
|
|
320
|
+
for (const [deviceId, device] of devices.entries()) {
|
|
321
|
+
if (device.synthetic === true) {
|
|
322
|
+
devices.delete(deviceId);
|
|
323
|
+
removed += 1;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return { ok: true, removed, total: devices.size };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function seedSyntheticFleet(options = {}) {
|
|
331
|
+
const count = clampNumber(options.count, 1, MAX_SYNTHETIC_DEVICES, 250);
|
|
332
|
+
const connectedRatio = Math.max(0, Math.min(1, Number(options.connectedRatio ?? 0.88)));
|
|
333
|
+
const thumbnailRatio = Math.max(0, Math.min(1, Number(options.thumbnailRatio ?? 0.7)));
|
|
334
|
+
const aiAssistRatio = Math.max(0, Math.min(1, Number(options.aiAssistRatio ?? 0.35)));
|
|
335
|
+
const liveCount = clampNumber(options.liveCount, 0, Math.min(count, 24), Math.min(6, count));
|
|
336
|
+
const replace = options.replace !== false;
|
|
337
|
+
const now = new Date();
|
|
338
|
+
const platforms = ['win32', 'linux', 'darwin'];
|
|
339
|
+
|
|
340
|
+
if (replace) {
|
|
341
|
+
clearSyntheticFleet();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let connected = 0;
|
|
345
|
+
let aiAssist = 0;
|
|
346
|
+
let live = 0;
|
|
347
|
+
for (let index = 0; index < count; index += 1) {
|
|
348
|
+
const ordinal = index + 1;
|
|
349
|
+
const deviceId = `synthetic-${String(ordinal).padStart(4, '0')}`;
|
|
350
|
+
const isConnected = index / count < connectedRatio;
|
|
351
|
+
const hasThumbnail = index / count < thumbnailRatio;
|
|
352
|
+
const hasAiAssist = index / count < aiAssistRatio;
|
|
353
|
+
const isLive = isConnected && index < liveCount;
|
|
354
|
+
const seenAt = new Date(now.getTime() - index * 1350).toISOString();
|
|
355
|
+
const connectedAt = new Date(now.getTime() - (index + 8) * 60000).toISOString();
|
|
356
|
+
const platform = platforms[index % platforms.length];
|
|
357
|
+
const usedMemRatio = Number((0.18 + (index % 71) / 100).toFixed(2));
|
|
358
|
+
const load1 = Number(((index % 19) / 3).toFixed(2));
|
|
359
|
+
const taskStatus = index % 17 === 0
|
|
360
|
+
? 'failed'
|
|
361
|
+
: index % 5 === 0
|
|
362
|
+
? 'completed'
|
|
363
|
+
: index % 7 === 0
|
|
364
|
+
? 'queued'
|
|
365
|
+
: '';
|
|
366
|
+
const latestTask = taskStatus
|
|
367
|
+
? {
|
|
368
|
+
taskId: `synthetic-task-${ordinal}`,
|
|
369
|
+
commandId: `synthetic-command-${ordinal}`,
|
|
370
|
+
title: taskStatus === 'failed' ? 'Synthetic issue check' : 'Synthetic status task',
|
|
371
|
+
instructionPreview: 'Synthetic fleet scale validation task.',
|
|
372
|
+
status: taskStatus,
|
|
373
|
+
approvalLevel: hasAiAssist ? 'ai-assist' : 'task-only',
|
|
374
|
+
requestedAt: seenAt,
|
|
375
|
+
sentAt: seenAt,
|
|
376
|
+
updatedAt: seenAt,
|
|
377
|
+
completedAt: taskStatus === 'completed' || taskStatus === 'failed' ? seenAt : '',
|
|
378
|
+
error: taskStatus === 'failed' ? 'Synthetic task failure sample.' : '',
|
|
379
|
+
resultKind: 'synthetic-agent-task',
|
|
380
|
+
resultSummary: taskStatus === 'failed'
|
|
381
|
+
? 'Synthetic task reported a sample issue.'
|
|
382
|
+
: 'Synthetic task completed for scale validation.'
|
|
383
|
+
}
|
|
384
|
+
: null;
|
|
385
|
+
const device = {
|
|
386
|
+
socket: null,
|
|
387
|
+
synthetic: true,
|
|
388
|
+
deviceId,
|
|
389
|
+
sessionId: `synthetic-session-${ordinal}`,
|
|
390
|
+
deviceName: `Synthetic PC ${String(ordinal).padStart(4, '0')}`,
|
|
391
|
+
hostname: `synthetic-host-${String(ordinal).padStart(4, '0')}`,
|
|
392
|
+
platform,
|
|
393
|
+
arch: index % 4 === 0 ? 'arm64' : 'x64',
|
|
394
|
+
pid: 0,
|
|
395
|
+
agentVersion: 'synthetic-scale',
|
|
396
|
+
capabilities: {
|
|
397
|
+
status: true,
|
|
398
|
+
thumbnail: hasThumbnail,
|
|
399
|
+
control: false,
|
|
400
|
+
liveStream: true,
|
|
401
|
+
computerAgent: true,
|
|
402
|
+
taskDispatch: true,
|
|
403
|
+
aiAssist: hasAiAssist,
|
|
404
|
+
aiModel: hasAiAssist ? 'synthetic-ai' : '',
|
|
405
|
+
aiProvider: hasAiAssist ? 'synthetic' : ''
|
|
406
|
+
},
|
|
407
|
+
connected: isConnected,
|
|
408
|
+
connectedAt: isConnected ? connectedAt : '',
|
|
409
|
+
disconnectedAt: isConnected ? '' : seenAt,
|
|
410
|
+
lastSeenAt: seenAt,
|
|
411
|
+
lastStatusAt: seenAt,
|
|
412
|
+
lastDisconnectReason: isConnected ? '' : 'synthetic-offline',
|
|
413
|
+
remoteAddress: 'synthetic.local',
|
|
414
|
+
remotePort: 0,
|
|
415
|
+
status: {
|
|
416
|
+
platform,
|
|
417
|
+
release: platform === 'win32' ? 'Windows 11' : platform === 'darwin' ? 'macOS' : 'Linux',
|
|
418
|
+
uptimeSec: (ordinal * 791) % 1209600,
|
|
419
|
+
usedMemRatio,
|
|
420
|
+
loadavg: [load1, Math.max(0, load1 - 0.2), Math.max(0, load1 - 0.4)]
|
|
421
|
+
},
|
|
422
|
+
latestThumbnail: null,
|
|
423
|
+
latestLiveFrame: null,
|
|
424
|
+
activeLiveStream: null,
|
|
425
|
+
latestTask,
|
|
426
|
+
recentTasks: latestTask ? [latestTask] : [],
|
|
427
|
+
pendingTaskCommands: new Map(),
|
|
428
|
+
counters: createDeviceCounters({
|
|
429
|
+
messagesReceived: 1,
|
|
430
|
+
statusReceived: 1,
|
|
431
|
+
tasksQueued: latestTask ? 1 : 0,
|
|
432
|
+
taskResultsReceived: latestTask && ['completed', 'failed'].includes(latestTask.status) ? 1 : 0,
|
|
433
|
+
taskResultsFailed: latestTask?.status === 'failed' ? 1 : 0
|
|
434
|
+
})
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
if (hasThumbnail) {
|
|
438
|
+
const frame = makeSyntheticFrame(device, `synthetic-thumb-${ordinal}`, 'thumbnail', {
|
|
439
|
+
width: 360,
|
|
440
|
+
height: 220
|
|
441
|
+
});
|
|
442
|
+
delete frame.mode;
|
|
443
|
+
delete frame.fps;
|
|
444
|
+
frame.receivedAt = seenAt;
|
|
445
|
+
frame.capturedAt = seenAt;
|
|
446
|
+
device.latestThumbnail = frame;
|
|
447
|
+
device.counters.thumbnailFramesReceived = 1;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (isLive) {
|
|
451
|
+
device.activeLiveStream = {
|
|
452
|
+
streamId: `synthetic-live-${ordinal}`,
|
|
453
|
+
commandId: `synthetic-live-command-${ordinal}`,
|
|
454
|
+
active: true,
|
|
455
|
+
mode: 'remote-fast',
|
|
456
|
+
fps: 12,
|
|
457
|
+
startedAt: seenAt,
|
|
458
|
+
stoppedAt: '',
|
|
459
|
+
stopReason: '',
|
|
460
|
+
lastFrameAt: '',
|
|
461
|
+
lastFrameSeq: 0,
|
|
462
|
+
framesReceived: 0
|
|
463
|
+
};
|
|
464
|
+
applySyntheticLiveFrame(device, device.activeLiveStream.streamId, {
|
|
465
|
+
commandId: device.activeLiveStream.commandId,
|
|
466
|
+
fps: 12
|
|
467
|
+
});
|
|
468
|
+
live += 1;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (isConnected) {
|
|
472
|
+
connected += 1;
|
|
473
|
+
}
|
|
474
|
+
if (hasAiAssist) {
|
|
475
|
+
aiAssist += 1;
|
|
476
|
+
}
|
|
477
|
+
devices.set(deviceId, device);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
emitRemoteEvent('RemoteSyntheticFleetSeeded', null, {
|
|
481
|
+
count,
|
|
482
|
+
connected,
|
|
483
|
+
aiAssist,
|
|
484
|
+
live
|
|
485
|
+
});
|
|
486
|
+
return {
|
|
487
|
+
ok: true,
|
|
488
|
+
synthetic: true,
|
|
489
|
+
seeded: count,
|
|
490
|
+
connected,
|
|
491
|
+
aiAssist,
|
|
492
|
+
live,
|
|
493
|
+
total: devices.size,
|
|
494
|
+
max: MAX_SYNTHETIC_DEVICES
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
225
498
|
function attachDevice(socket, hello) {
|
|
226
499
|
const now = new Date().toISOString();
|
|
227
500
|
const sessionId = crypto.randomUUID();
|
|
@@ -257,21 +530,8 @@ export function createRemoteHub(options = {}) {
|
|
|
257
530
|
latestTask: null,
|
|
258
531
|
recentTasks: [],
|
|
259
532
|
pendingTaskCommands: new Map(),
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
statusReceived: 0,
|
|
263
|
-
commandsSent: 0,
|
|
264
|
-
commandResultsReceived: 0,
|
|
265
|
-
thumbnailFramesReceived: 0,
|
|
266
|
-
thumbnailFramesDropped: 0,
|
|
267
|
-
liveFramesReceived: 0,
|
|
268
|
-
liveFramesDropped: 0,
|
|
269
|
-
liveStreamsStarted: 0,
|
|
270
|
-
liveStreamsStopped: 0,
|
|
271
|
-
tasksQueued: 0,
|
|
272
|
-
taskResultsReceived: 0,
|
|
273
|
-
taskResultsFailed: 0
|
|
274
|
-
}
|
|
533
|
+
synthetic: false,
|
|
534
|
+
counters: createDeviceCounters({ messagesReceived: 1 })
|
|
275
535
|
};
|
|
276
536
|
|
|
277
537
|
devices.set(deviceId, device);
|
|
@@ -685,6 +945,19 @@ export function createRemoteHub(options = {}) {
|
|
|
685
945
|
|
|
686
946
|
function disconnectDevice(deviceId, reason = 'manager-disconnect') {
|
|
687
947
|
const device = devices.get(String(deviceId || ''));
|
|
948
|
+
if (device?.synthetic === true) {
|
|
949
|
+
device.connected = false;
|
|
950
|
+
device.disconnectedAt = new Date().toISOString();
|
|
951
|
+
device.lastDisconnectReason = reason;
|
|
952
|
+
if (device.activeLiveStream) {
|
|
953
|
+
device.activeLiveStream.active = false;
|
|
954
|
+
device.activeLiveStream.stoppedAt = device.disconnectedAt;
|
|
955
|
+
device.activeLiveStream.stopReason = reason;
|
|
956
|
+
}
|
|
957
|
+
emitRemoteEvent('RemoteDeviceDisconnected', device, { reason, synthetic: true });
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
960
|
+
|
|
688
961
|
if (!device?.socket || device.socket.destroyed) {
|
|
689
962
|
return false;
|
|
690
963
|
}
|
|
@@ -696,6 +969,29 @@ export function createRemoteHub(options = {}) {
|
|
|
696
969
|
|
|
697
970
|
function sendCommand(deviceId, command) {
|
|
698
971
|
const device = devices.get(String(deviceId || ''));
|
|
972
|
+
if (device?.synthetic === true && device.connected) {
|
|
973
|
+
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
974
|
+
device.counters.commandsSent += 1;
|
|
975
|
+
device.lastSeenAt = new Date().toISOString();
|
|
976
|
+
if (command?.command === 'thumbnail.capture') {
|
|
977
|
+
applySyntheticThumbnail(device, {
|
|
978
|
+
commandId,
|
|
979
|
+
payload: command?.payload || {}
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
983
|
+
commandId,
|
|
984
|
+
command: safeString(command?.command || 'ping', 80),
|
|
985
|
+
synthetic: true
|
|
986
|
+
});
|
|
987
|
+
emitRemoteEvent('RemoteCommandResult', device, {
|
|
988
|
+
commandId,
|
|
989
|
+
result: { ok: true, synthetic: true },
|
|
990
|
+
error: ''
|
|
991
|
+
});
|
|
992
|
+
return { ok: true, commandId, synthetic: true };
|
|
993
|
+
}
|
|
994
|
+
|
|
699
995
|
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
700
996
|
return { ok: false, error: 'device-not-connected' };
|
|
701
997
|
}
|
|
@@ -720,6 +1016,65 @@ export function createRemoteHub(options = {}) {
|
|
|
720
1016
|
|
|
721
1017
|
function requestAgentTask(deviceId, options = {}) {
|
|
722
1018
|
const device = devices.get(String(deviceId || ''));
|
|
1019
|
+
if (device?.synthetic === true && device.connected) {
|
|
1020
|
+
const instruction = safeText(options.instruction, MAX_AGENT_TASK_CHARS);
|
|
1021
|
+
if (!instruction) {
|
|
1022
|
+
return { ok: false, error: 'missing-instruction' };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const now = new Date().toISOString();
|
|
1026
|
+
const approvalLevel = normalizeApprovalLevel(options.approvalLevel);
|
|
1027
|
+
if (approvalLevel === 'ai-assist' && !readCapabilityFlag(device.capabilities, 'aiAssist')) {
|
|
1028
|
+
return { ok: false, error: 'device-ai-assist-unavailable' };
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
1032
|
+
const taskId = safeString(options.taskId, 128) || crypto.randomUUID();
|
|
1033
|
+
const title = safeString(options.title, 120)
|
|
1034
|
+
|| safeString(instruction.split(/\r?\n/)[0], 120)
|
|
1035
|
+
|| 'Remote task';
|
|
1036
|
+
const task = {
|
|
1037
|
+
taskId,
|
|
1038
|
+
commandId,
|
|
1039
|
+
title,
|
|
1040
|
+
instructionPreview: safeText(instruction, 320),
|
|
1041
|
+
status: 'completed',
|
|
1042
|
+
approvalLevel,
|
|
1043
|
+
requestedAt: now,
|
|
1044
|
+
sentAt: now,
|
|
1045
|
+
updatedAt: now,
|
|
1046
|
+
completedAt: now,
|
|
1047
|
+
error: '',
|
|
1048
|
+
resultKind: approvalLevel === 'ai-assist' ? 'synthetic-ai-assist' : 'synthetic-agent-task',
|
|
1049
|
+
resultSummary: approvalLevel === 'ai-assist'
|
|
1050
|
+
? `Synthetic AI assist completed for ${device.deviceName}.`
|
|
1051
|
+
: `Synthetic task accepted by ${device.deviceName}.`
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
device.counters.commandsSent += 1;
|
|
1055
|
+
device.counters.commandResultsReceived += 1;
|
|
1056
|
+
device.counters.tasksQueued += 1;
|
|
1057
|
+
device.counters.taskResultsReceived += 1;
|
|
1058
|
+
device.lastSeenAt = now;
|
|
1059
|
+
rememberDeviceTask(device, task);
|
|
1060
|
+
device.pendingTaskCommands.delete(commandId);
|
|
1061
|
+
emitRemoteEvent('RemoteTaskQueued', device, {
|
|
1062
|
+
commandId,
|
|
1063
|
+
taskId,
|
|
1064
|
+
title,
|
|
1065
|
+
approvalLevel,
|
|
1066
|
+
synthetic: true
|
|
1067
|
+
});
|
|
1068
|
+
emitRemoteEvent('RemoteTaskResult', device, {
|
|
1069
|
+
commandId,
|
|
1070
|
+
taskId,
|
|
1071
|
+
status: task.status,
|
|
1072
|
+
error: '',
|
|
1073
|
+
synthetic: true
|
|
1074
|
+
});
|
|
1075
|
+
return { ok: true, commandId, taskId, approvalLevel, synthetic: true };
|
|
1076
|
+
}
|
|
1077
|
+
|
|
723
1078
|
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
724
1079
|
return { ok: false, error: 'device-not-connected' };
|
|
725
1080
|
}
|
|
@@ -803,6 +1158,45 @@ export function createRemoteHub(options = {}) {
|
|
|
803
1158
|
|
|
804
1159
|
function startLiveStream(deviceId, options = {}) {
|
|
805
1160
|
const device = devices.get(String(deviceId || ''));
|
|
1161
|
+
if (device?.synthetic === true && device.connected) {
|
|
1162
|
+
if (!readCapabilityFlag(device.capabilities, 'liveStream')) {
|
|
1163
|
+
return { ok: false, error: 'device-live-stream-unavailable' };
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const now = new Date().toISOString();
|
|
1167
|
+
const streamId = safeString(options.streamId, 128) || `live-${Date.now()}`;
|
|
1168
|
+
const fps = clampNumber(options.fps, 1, 24, 12);
|
|
1169
|
+
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
1170
|
+
device.counters.commandsSent += 1;
|
|
1171
|
+
device.activeLiveStream = {
|
|
1172
|
+
streamId,
|
|
1173
|
+
commandId,
|
|
1174
|
+
active: true,
|
|
1175
|
+
mode: 'remote-fast',
|
|
1176
|
+
fps,
|
|
1177
|
+
startedAt: now,
|
|
1178
|
+
stoppedAt: '',
|
|
1179
|
+
stopReason: '',
|
|
1180
|
+
lastFrameAt: '',
|
|
1181
|
+
lastFrameSeq: 0,
|
|
1182
|
+
framesReceived: 0
|
|
1183
|
+
};
|
|
1184
|
+
device.counters.liveStreamsStarted += 1;
|
|
1185
|
+
applySyntheticLiveFrame(device, streamId, {
|
|
1186
|
+
commandId,
|
|
1187
|
+
maxWidth: options.maxWidth,
|
|
1188
|
+
maxHeight: options.maxHeight,
|
|
1189
|
+
fps
|
|
1190
|
+
});
|
|
1191
|
+
emitRemoteEvent('RemoteLiveStreamStarted', device, {
|
|
1192
|
+
streamId,
|
|
1193
|
+
commandId,
|
|
1194
|
+
fps,
|
|
1195
|
+
synthetic: true
|
|
1196
|
+
});
|
|
1197
|
+
return { ok: true, commandId, streamId, fps, synthetic: true };
|
|
1198
|
+
}
|
|
1199
|
+
|
|
806
1200
|
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
807
1201
|
return { ok: false, error: 'device-not-connected' };
|
|
808
1202
|
}
|
|
@@ -857,6 +1251,24 @@ export function createRemoteHub(options = {}) {
|
|
|
857
1251
|
|
|
858
1252
|
function stopLiveStream(deviceId, options = {}) {
|
|
859
1253
|
const device = devices.get(String(deviceId || ''));
|
|
1254
|
+
if (device?.synthetic === true && device.connected) {
|
|
1255
|
+
const streamId = safeString(options.streamId, 128) || device.activeLiveStream?.streamId || '';
|
|
1256
|
+
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
1257
|
+
device.counters.commandsSent += 1;
|
|
1258
|
+
if (device.activeLiveStream && (!streamId || device.activeLiveStream.streamId === streamId)) {
|
|
1259
|
+
device.activeLiveStream.active = false;
|
|
1260
|
+
device.activeLiveStream.stoppedAt = new Date().toISOString();
|
|
1261
|
+
device.activeLiveStream.stopReason = 'manager-request';
|
|
1262
|
+
}
|
|
1263
|
+
device.counters.liveStreamsStopped += 1;
|
|
1264
|
+
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
1265
|
+
streamId,
|
|
1266
|
+
commandId,
|
|
1267
|
+
synthetic: true
|
|
1268
|
+
});
|
|
1269
|
+
return { ok: true, commandId, streamId, synthetic: true };
|
|
1270
|
+
}
|
|
1271
|
+
|
|
860
1272
|
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
861
1273
|
return { ok: false, error: 'device-not-connected' };
|
|
862
1274
|
}
|
|
@@ -913,6 +1325,8 @@ export function createRemoteHub(options = {}) {
|
|
|
913
1325
|
stopLiveStream,
|
|
914
1326
|
getDeviceLiveFrame,
|
|
915
1327
|
getDeviceThumbnail,
|
|
1328
|
+
seedSyntheticFleet,
|
|
1329
|
+
clearSyntheticFleet,
|
|
916
1330
|
getPairToken: () => pairToken
|
|
917
1331
|
};
|
|
918
1332
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from 'assert/strict';
|
|
4
|
+
import { createRemoteHub } from '../remote-hub.js';
|
|
5
|
+
|
|
6
|
+
const SYNTHETIC_COUNT = Number(process.env.REMOTE_HUB_SCALE_SMOKE_COUNT || 250);
|
|
7
|
+
|
|
8
|
+
const hub = createRemoteHub({
|
|
9
|
+
env: {
|
|
10
|
+
MINDEXEC_REMOTE_HUB: '1',
|
|
11
|
+
REMOTE_HUB_HOST: '127.0.0.1',
|
|
12
|
+
REMOTE_HUB_PORT: '0',
|
|
13
|
+
REMOTE_HUB_PAIR_TOKEN: 'scale-smoke-token'
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await hub.start();
|
|
19
|
+
|
|
20
|
+
const seeded = hub.seedSyntheticFleet({
|
|
21
|
+
count: SYNTHETIC_COUNT,
|
|
22
|
+
connectedRatio: 0.84,
|
|
23
|
+
thumbnailRatio: 0.72,
|
|
24
|
+
aiAssistRatio: 0.42,
|
|
25
|
+
liveCount: 8
|
|
26
|
+
});
|
|
27
|
+
assert.equal(seeded.ok, true);
|
|
28
|
+
assert.equal(seeded.seeded, SYNTHETIC_COUNT);
|
|
29
|
+
|
|
30
|
+
const status = hub.getStatus();
|
|
31
|
+
assert.equal(status.started, true);
|
|
32
|
+
assert.equal(status.canvasPagination, 'none');
|
|
33
|
+
assert.equal(status.deviceCount, SYNTHETIC_COUNT);
|
|
34
|
+
assert.equal(status.connectedDeviceCount, seeded.connected);
|
|
35
|
+
|
|
36
|
+
const devices = hub.listDevices();
|
|
37
|
+
assert.equal(devices.length, SYNTHETIC_COUNT);
|
|
38
|
+
assert.equal(devices.filter(device => device.synthetic === true).length, SYNTHETIC_COUNT);
|
|
39
|
+
assert.equal(devices.some(device => device.connected === false), true);
|
|
40
|
+
assert.equal(devices.some(device => device.latestThumbnail?.dataUrl), true);
|
|
41
|
+
assert.equal(devices.some(device => device.activeLiveStream?.active === true), true);
|
|
42
|
+
|
|
43
|
+
const deviceListPayload = {
|
|
44
|
+
total: devices.length,
|
|
45
|
+
pagination: 'none',
|
|
46
|
+
canvasDeviceListMode: 'all-devices',
|
|
47
|
+
devices
|
|
48
|
+
};
|
|
49
|
+
assert.ok(JSON.stringify(deviceListPayload).length < 8 * 1024 * 1024);
|
|
50
|
+
|
|
51
|
+
const thumbnailTarget = devices.find(device => device.connected && device.capabilities?.thumbnail);
|
|
52
|
+
assert.ok(thumbnailTarget);
|
|
53
|
+
const thumbnailResult = hub.requestThumbnail(thumbnailTarget.deviceId, {
|
|
54
|
+
streamId: 'scale-thumb',
|
|
55
|
+
maxWidth: 360,
|
|
56
|
+
maxHeight: 220,
|
|
57
|
+
quality: 50
|
|
58
|
+
});
|
|
59
|
+
assert.equal(thumbnailResult.ok, true);
|
|
60
|
+
assert.equal(hub.getDeviceThumbnail(thumbnailTarget.deviceId).streamId, 'scale-thumb');
|
|
61
|
+
|
|
62
|
+
const liveTarget = devices.find(device => device.connected && device.capabilities?.liveStream);
|
|
63
|
+
assert.ok(liveTarget);
|
|
64
|
+
const liveResult = hub.startLiveStream(liveTarget.deviceId, {
|
|
65
|
+
streamId: 'scale-live',
|
|
66
|
+
fps: 12,
|
|
67
|
+
maxWidth: 960,
|
|
68
|
+
maxHeight: 540,
|
|
69
|
+
quality: 60
|
|
70
|
+
});
|
|
71
|
+
assert.equal(liveResult.ok, true);
|
|
72
|
+
assert.equal(hub.getDeviceLiveFrame(liveTarget.deviceId).streamId, 'scale-live');
|
|
73
|
+
const stopResult = hub.stopLiveStream(liveTarget.deviceId, { streamId: 'scale-live' });
|
|
74
|
+
assert.equal(stopResult.ok, true);
|
|
75
|
+
|
|
76
|
+
const connectedTargets = hub.listDevices()
|
|
77
|
+
.filter(device => device.connected && device.capabilities?.taskDispatch)
|
|
78
|
+
.slice(0, 200);
|
|
79
|
+
assert.ok(connectedTargets.length >= 100);
|
|
80
|
+
const taskResults = connectedTargets.map(device =>
|
|
81
|
+
hub.requestAgentTask(device.deviceId, {
|
|
82
|
+
instruction: 'Synthetic scale task: report current status to the manager.',
|
|
83
|
+
title: 'Scale task'
|
|
84
|
+
}));
|
|
85
|
+
assert.equal(taskResults.every(result => result.ok === true), true);
|
|
86
|
+
assert.equal(taskResults.length, connectedTargets.length);
|
|
87
|
+
|
|
88
|
+
const aiTarget = hub.listDevices().find(device =>
|
|
89
|
+
device.connected && device.capabilities?.taskDispatch && device.capabilities?.aiAssist);
|
|
90
|
+
assert.ok(aiTarget);
|
|
91
|
+
const aiResult = hub.requestAgentTask(aiTarget.deviceId, {
|
|
92
|
+
instruction: 'Synthetic AI assist: summarize fleet condition.',
|
|
93
|
+
title: 'Scale AI task',
|
|
94
|
+
approvalLevel: 'ai-assist'
|
|
95
|
+
});
|
|
96
|
+
assert.equal(aiResult.ok, true);
|
|
97
|
+
assert.equal(aiResult.approvalLevel, 'ai-assist');
|
|
98
|
+
assert.equal(hub.listDevices().find(device => device.deviceId === aiTarget.deviceId)
|
|
99
|
+
?.latestTask?.approvalLevel, 'ai-assist');
|
|
100
|
+
|
|
101
|
+
const cleared = hub.clearSyntheticFleet();
|
|
102
|
+
assert.equal(cleared.ok, true);
|
|
103
|
+
assert.equal(cleared.removed, SYNTHETIC_COUNT);
|
|
104
|
+
assert.equal(hub.listDevices().length, 0);
|
|
105
|
+
|
|
106
|
+
console.log(`RemoteHub scale smoke OK (${SYNTHETIC_COUNT} synthetic devices)`);
|
|
107
|
+
} finally {
|
|
108
|
+
await hub.close();
|
|
109
|
+
}
|
package/server.js
CHANGED
|
@@ -6973,6 +6973,38 @@ app.get('/api/remote/devices', (req, res) => {
|
|
|
6973
6973
|
});
|
|
6974
6974
|
});
|
|
6975
6975
|
|
|
6976
|
+
function isSyntheticRemoteFleetEnabled() {
|
|
6977
|
+
return /^(1|true|yes|on)$/i.test(String(
|
|
6978
|
+
process.env.MINDEXEC_REMOTE_SYNTHETIC_FLEET
|
|
6979
|
+
|| process.env.REMOTE_HUB_SYNTHETIC_FLEET
|
|
6980
|
+
|| ''));
|
|
6981
|
+
}
|
|
6982
|
+
|
|
6983
|
+
app.post('/api/remote/synthetic/seed', (req, res) => {
|
|
6984
|
+
if (!isSyntheticRemoteFleetEnabled()) {
|
|
6985
|
+
res.status(403).json({ ok: false, error: 'synthetic-fleet-disabled' });
|
|
6986
|
+
return;
|
|
6987
|
+
}
|
|
6988
|
+
|
|
6989
|
+
res.json(remoteHub.seedSyntheticFleet({
|
|
6990
|
+
count: req.body?.count,
|
|
6991
|
+
connectedRatio: req.body?.connectedRatio,
|
|
6992
|
+
thumbnailRatio: req.body?.thumbnailRatio,
|
|
6993
|
+
aiAssistRatio: req.body?.aiAssistRatio,
|
|
6994
|
+
liveCount: req.body?.liveCount,
|
|
6995
|
+
replace: req.body?.replace
|
|
6996
|
+
}));
|
|
6997
|
+
});
|
|
6998
|
+
|
|
6999
|
+
app.delete('/api/remote/synthetic', (req, res) => {
|
|
7000
|
+
if (!isSyntheticRemoteFleetEnabled()) {
|
|
7001
|
+
res.status(403).json({ ok: false, error: 'synthetic-fleet-disabled' });
|
|
7002
|
+
return;
|
|
7003
|
+
}
|
|
7004
|
+
|
|
7005
|
+
res.json(remoteHub.clearSyntheticFleet());
|
|
7006
|
+
});
|
|
7007
|
+
|
|
6976
7008
|
app.post('/api/remote/devices/:deviceId/disconnect', (req, res) => {
|
|
6977
7009
|
const disconnected = remoteHub.disconnectDevice(req.params.deviceId, 'manager-request');
|
|
6978
7010
|
res.json({ ok: disconnected });
|