mindexec-ai 0.2.385

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.
Files changed (99) hide show
  1. package/README.md +354 -0
  2. package/codex-runtime.js +1339 -0
  3. package/launch-bridge.cjs +236 -0
  4. package/package.json +77 -0
  5. package/port-guard.cjs +232 -0
  6. package/remote-fast/osx-arm64/mindexec-remote-fast +0 -0
  7. package/remote-fast/osx-arm64/mindexec-remote-fast.deps.json +24 -0
  8. package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
  9. package/remote-fast/osx-arm64/mindexec-remote-fast.runtimeconfig.json +13 -0
  10. package/remote-fast/osx-x64/mindexec-remote-fast +0 -0
  11. package/remote-fast/osx-x64/mindexec-remote-fast.deps.json +24 -0
  12. package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
  13. package/remote-fast/osx-x64/mindexec-remote-fast.runtimeconfig.json +13 -0
  14. package/remote-fast/win-x64/mindexec-remote-fast.deps.json +24 -0
  15. package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
  16. package/remote-fast/win-x64/mindexec-remote-fast.exe +0 -0
  17. package/remote-fast/win-x64/mindexec-remote-fast.runtimeconfig.json +20 -0
  18. package/remote-hub.js +3106 -0
  19. package/scripts/auth-session-smoke.mjs +262 -0
  20. package/scripts/remote-agent-managed-smoke.mjs +291 -0
  21. package/scripts/remote-agent-package-smoke.mjs +64 -0
  22. package/scripts/remote-agent-ws-smoke.mjs +202 -0
  23. package/scripts/remote-fast-live-rate-smoke.mjs +355 -0
  24. package/scripts/remote-fast-mdm-browser-smoke.mjs +476 -0
  25. package/scripts/remote-fleet-render-smoke.mjs +1491 -0
  26. package/scripts/remote-frame-ws-smoke.mjs +234 -0
  27. package/scripts/remote-http-smoke.mjs +592 -0
  28. package/scripts/remote-hub-identity-smoke.mjs +146 -0
  29. package/scripts/remote-hub-scale-smoke.mjs +124 -0
  30. package/scripts/remote-hub-smoke.mjs +631 -0
  31. package/scripts/remote-input-ws-smoke.mjs +263 -0
  32. package/scripts/remote-registry-follower-smoke.mjs +752 -0
  33. package/scripts/setup-tree-sitter-grammars.mjs +80 -0
  34. package/server.js +15709 -0
  35. package/start-bridge.bat +32 -0
  36. package/start-bridge.sh +81 -0
  37. package/tree-sitter-grammars/README.md +18 -0
  38. package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
  39. package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
  40. package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
  41. package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
  42. package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
  43. package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
  44. package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
  45. package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
  46. package/wwwroot/_headers +73 -0
  47. package/wwwroot/_redirects +1 -0
  48. package/wwwroot/appsettings.json +83 -0
  49. package/wwwroot/assets/AdminDashboardPage-B2vz2Px9.css +1 -0
  50. package/wwwroot/assets/AdminDashboardPage-DnuCHywn.js +1 -0
  51. package/wwwroot/assets/AppSidebar-DU2OgSiv.js +2 -0
  52. package/wwwroot/assets/AuthPages-BrH6kRcv.css +1 -0
  53. package/wwwroot/assets/AuthPages-Dgezl7Vj.js +1 -0
  54. package/wwwroot/assets/CodePage-7kgZlB3O.js +87 -0
  55. package/wwwroot/assets/CodePage-Bncc352E.css +1 -0
  56. package/wwwroot/assets/CompanyCorePage-ChBnq1ve.css +1 -0
  57. package/wwwroot/assets/CompanyCorePage-CzIZIIU_.js +13 -0
  58. package/wwwroot/assets/ExecutionModePage-B-etp_mc.js +18 -0
  59. package/wwwroot/assets/ExecutionModePage-TLuld9l3.css +1 -0
  60. package/wwwroot/assets/LaunchLeadCapture-Bx9LM0IX.js +1 -0
  61. package/wwwroot/assets/LaunchLeadCapture-CiRI1shz.css +1 -0
  62. package/wwwroot/assets/MarketingHome-BsyerRpe.js +1 -0
  63. package/wwwroot/assets/MarketingHome-DPzaYzA_.css +1 -0
  64. package/wwwroot/assets/MindCanvas-DtqOZnoW.css +1 -0
  65. package/wwwroot/assets/MindCanvas-zEDXzaxW.js +49 -0
  66. package/wwwroot/assets/PlanMasterPage-CJ36rep-.css +1 -0
  67. package/wwwroot/assets/PlanMasterPage-NZ_mPvaE.js +4 -0
  68. package/wwwroot/assets/PricingPage-Cg_0i_ZR.css +1 -0
  69. package/wwwroot/assets/PricingPage-Ylrn8l2g.js +1 -0
  70. package/wwwroot/assets/ToolPages-3M2KqA9k.js +28 -0
  71. package/wwwroot/assets/ToolPages-DIB187pZ.css +1 -0
  72. package/wwwroot/assets/YouTubeSearchPage-COv1oAA7.js +4 -0
  73. package/wwwroot/assets/YouTubeSearchPage-IPPa_BIH.css +1 -0
  74. package/wwwroot/assets/app-runtime-xD2Z3NdN.js +1 -0
  75. package/wwwroot/assets/canvas-runtime-BbicBcOj.js +44 -0
  76. package/wwwroot/assets/code-agent-runtime-B5PPZd1t.js +74 -0
  77. package/wwwroot/assets/executionModeSettings-NJqurj-o.js +1 -0
  78. package/wwwroot/assets/index-CQMKCp-t.js +2 -0
  79. package/wwwroot/assets/index-yNpEK-gp.css +1 -0
  80. package/wwwroot/assets/marketingTools-DN_rnHeB.js +4 -0
  81. package/wwwroot/assets/mindCanvasSearchWorker-BzPMsHOB.js +1 -0
  82. package/wwwroot/assets/mindexecution-mindcanvas.png +0 -0
  83. package/wwwroot/assets/mindexecution-prod-home-current.png +0 -0
  84. package/wwwroot/assets/mindexecution-prod-pricing-current.png +0 -0
  85. package/wwwroot/assets/pricingCheckoutShell-O-DnwmbU.js +1 -0
  86. package/wwwroot/assets/productionAdapterConfig-C5jfk6oG.js +1 -0
  87. package/wwwroot/assets/runtimeSettingsPersistenceProjection-BoNWmYjU.js +1 -0
  88. package/wwwroot/assets/storage-TM3YrWaj.js +1 -0
  89. package/wwwroot/assets/supabaseAuthAdapter-DA43DeSY.js +44 -0
  90. package/wwwroot/assets/toolHandoff-D5e5f7t5.js +4 -0
  91. package/wwwroot/assets/vendor-icons-DE3gIReG.js +681 -0
  92. package/wwwroot/assets/vendor-msgpack-BE8aAsr3.js +1 -0
  93. package/wwwroot/assets/vendor-react-BXzpOyCS.js +40 -0
  94. package/wwwroot/favicon.svg +7 -0
  95. package/wwwroot/index.html +22 -0
  96. package/wwwroot/manifest.webmanifest +19 -0
  97. package/wwwroot/robots.txt +4 -0
  98. package/wwwroot/service-worker.js +7 -0
  99. package/wwwroot/sitemap.xml +39 -0
package/remote-hub.js ADDED
@@ -0,0 +1,3106 @@
1
+ import net from 'net';
2
+ import os from 'os';
3
+ import crypto from 'crypto';
4
+
5
+ const DEFAULT_REMOTE_HUB_PORT = 5197;
6
+ const DEFAULT_REMOTE_HUB_HOST = '0.0.0.0';
7
+ const DEFAULT_HEARTBEAT_MS = 5000;
8
+ const DEFAULT_AGENT_TASK_TIMEOUT_MS = 120000;
9
+ const MAX_LINE_CHARS = 4 * 1024 * 1024;
10
+ const MAX_THUMBNAIL_BASE64_CHARS = 3 * 1024 * 1024;
11
+ const MAX_STREAM_BASE64_CHARS = 3 * 1024 * 1024;
12
+ const MAX_THUMBNAIL_BINARY_BYTES = Math.floor(MAX_THUMBNAIL_BASE64_CHARS * 3 / 4);
13
+ const MAX_STREAM_BINARY_BYTES = Math.floor(MAX_STREAM_BASE64_CHARS * 3 / 4);
14
+ const MAX_AGENT_TASK_CHARS = 4000;
15
+ const MAX_AGENT_TASK_RESULT_CHARS = 3000;
16
+ const RECENT_TASK_LIMIT = 12;
17
+ const RECENT_TASK_BATCH_LIMIT = 16;
18
+ const RECENT_FRAME_CACHE_TTL_MS = 12000;
19
+ const RECENT_THUMBNAIL_FRAME_CACHE_LIMIT = 4;
20
+ const RECENT_LIVE_FRAME_CACHE_LIMIT = 80;
21
+ const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 3 * 1024 * 1024;
22
+ const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 24 * 1024 * 1024;
23
+ const REMOTE_PROTOCOL_VERSION = 1;
24
+ const MAX_SYNTHETIC_DEVICES = 1000;
25
+ const DEFAULT_HOST_TARGET_LEASE_MS = 30000;
26
+ const SYNTHETIC_FRAME_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC';
27
+ const SYNTHETIC_FRAME_PAYLOAD = Buffer.from(SYNTHETIC_FRAME_DATA_URL.split(',')[1], 'base64');
28
+ const SYNTHETIC_FRAME_HASH = crypto.createHash('sha256').update(SYNTHETIC_FRAME_PAYLOAD).digest('hex').slice(0, 16);
29
+
30
+ function isEnabledValue(value, fallback = true) {
31
+ if (value === undefined || value === null || value === '') {
32
+ return fallback;
33
+ }
34
+
35
+ return /^(1|true|yes|on)$/i.test(String(value).trim());
36
+ }
37
+
38
+ function isDisabledValue(value) {
39
+ return /^(1|true|yes|on)$/i.test(String(value || '').trim());
40
+ }
41
+
42
+ function clampNumber(value, min, max, fallback) {
43
+ const number = Number(value);
44
+ if (!Number.isFinite(number)) {
45
+ return fallback;
46
+ }
47
+
48
+ return Math.max(min, Math.min(max, Math.floor(number)));
49
+ }
50
+
51
+ function normalizePort(value) {
52
+ return clampNumber(value, 0, 65535, DEFAULT_REMOTE_HUB_PORT);
53
+ }
54
+
55
+ function safeString(value, maxLength = 200) {
56
+ return String(value ?? '').replace(/[\r\n\t]/g, ' ').trim().slice(0, maxLength);
57
+ }
58
+
59
+ function isLoopbackHost(value) {
60
+ const host = String(value || '').trim().toLowerCase();
61
+ return host === 'localhost'
62
+ || host === '127.0.0.1'
63
+ || host === '::1'
64
+ || host === '[::1]';
65
+ }
66
+
67
+ function isWildcardHost(value) {
68
+ const host = String(value || '').trim().toLowerCase();
69
+ return host === '0.0.0.0'
70
+ || host === '::'
71
+ || host === '[::]'
72
+ || host === '';
73
+ }
74
+
75
+ function isPrivateIPv4(value) {
76
+ const parts = String(value || '').split('.').map(part => Number(part));
77
+ if (parts.length !== 4 || parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) {
78
+ return false;
79
+ }
80
+
81
+ return parts[0] === 10
82
+ || (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
83
+ || (parts[0] === 192 && parts[1] === 168);
84
+ }
85
+
86
+ function isPreferredPhysicalInterfaceName(value) {
87
+ const name = String(value || '').toLowerCase();
88
+ return /(^|[\s_\-()])((wi[\s_\-]?fi)|wifi|wireless|wlan|ethernet|lan|이더넷|en\d+|eth\d+)(?=$|[\s_\-()])/i.test(name);
89
+ }
90
+
91
+ function isVirtualInterfaceName(value) {
92
+ const name = String(value || '').toLowerCase();
93
+ return /virtual|vethernet|hyper-v|hyperv|vmware|virtualbox|vbox|docker|wsl|container|bluetooth|loopback|npcap|isatap|teredo/i.test(name);
94
+ }
95
+
96
+ function getIPv4LastOctet(value) {
97
+ const parts = String(value || '').split('.').map(part => Number(part));
98
+ if (parts.length !== 4 || parts.some(part => !Number.isInteger(part))) {
99
+ return -1;
100
+ }
101
+
102
+ return parts[3];
103
+ }
104
+
105
+ function scoreReachableIPv4Candidate(candidate) {
106
+ const address = String(candidate?.address || '');
107
+ const name = String(candidate?.name || '');
108
+ let score = 0;
109
+
110
+ if (isPrivateIPv4(address)) {
111
+ score += 1000;
112
+ }
113
+
114
+ if (/^192\.168\./.test(address)) {
115
+ score += 90;
116
+ } else if (/^10\./.test(address)) {
117
+ score += 80;
118
+ } else if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(address)) {
119
+ score += 70;
120
+ }
121
+
122
+ if (isPreferredPhysicalInterfaceName(name)) {
123
+ score += 500;
124
+ }
125
+
126
+ if (isVirtualInterfaceName(name)) {
127
+ score -= 1200;
128
+ }
129
+
130
+ if (/^169\.254\./.test(address)) {
131
+ score -= 2000;
132
+ }
133
+
134
+ const lastOctet = getIPv4LastOctet(address);
135
+ if (lastOctet === 1) {
136
+ score -= 60;
137
+ } else if (lastOctet === 0 || lastOctet === 255) {
138
+ score -= 100;
139
+ }
140
+
141
+ if (candidate?.mac && candidate.mac === '00:00:00:00:00:00') {
142
+ score -= 50;
143
+ }
144
+
145
+ return score;
146
+ }
147
+
148
+ function getReachableIPv4CandidateEntries() {
149
+ const candidates = [];
150
+ const interfaces = os.networkInterfaces();
151
+ for (const [name, entries] of Object.entries(interfaces)) {
152
+ for (const entry of entries || []) {
153
+ if (entry?.family !== 'IPv4' || entry.internal) {
154
+ continue;
155
+ }
156
+
157
+ const address = safeString(entry.address, 64);
158
+ if (!address || address === '0.0.0.0') {
159
+ continue;
160
+ }
161
+
162
+ candidates.push({
163
+ address,
164
+ name: safeString(name, 128),
165
+ mac: safeString(entry.mac, 32),
166
+ score: 0,
167
+ virtual: isVirtualInterfaceName(name),
168
+ preferred: isPreferredPhysicalInterfaceName(name)
169
+ });
170
+ }
171
+ }
172
+
173
+ return candidates
174
+ .map(candidate => ({
175
+ ...candidate,
176
+ score: scoreReachableIPv4Candidate(candidate)
177
+ }))
178
+ .sort((left, right) => right.score - left.score || left.address.localeCompare(right.address))
179
+ .filter((candidate, index, all) => all.findIndex(item => item.address === candidate.address) === index);
180
+ }
181
+
182
+ function getReachableIPv4Candidates() {
183
+ return getReachableIPv4CandidateEntries().map(candidate => candidate.address);
184
+ }
185
+
186
+ function getReachableIPv4Address() {
187
+ const candidates = getReachableIPv4Candidates();
188
+ return candidates.find(isPrivateIPv4) || candidates[0] || '127.0.0.1';
189
+ }
190
+
191
+ function buildEndpoint(hostValue, portValue) {
192
+ const hostText = safeString(hostValue, 128);
193
+ const port = normalizePort(portValue);
194
+ if (!hostText || !port) {
195
+ return '';
196
+ }
197
+
198
+ const hostPart = hostText.includes(':') && !hostText.startsWith('[')
199
+ ? `[${hostText}]`
200
+ : hostText;
201
+ return `${hostPart}:${port}`;
202
+ }
203
+
204
+ function parseManagerEndpoint(value) {
205
+ const endpoint = safeString(value, 256);
206
+ const match = endpoint.match(/^(\[[^\]]+\]|[^:\s]+):(\d{1,5})$/);
207
+ if (!match) {
208
+ return null;
209
+ }
210
+
211
+ const host = match[1].replace(/^\[|\]$/g, '');
212
+ const port = Number(match[2]);
213
+ if (!host || !Number.isInteger(port) || port <= 0 || port > 65535) {
214
+ return null;
215
+ }
216
+
217
+ return { endpoint, host, port };
218
+ }
219
+
220
+ function getAccountRouteEndpointReason(endpoint, context = {}) {
221
+ const parsed = parseManagerEndpoint(endpoint);
222
+ if (!parsed) {
223
+ return 'invalid-manager-endpoint';
224
+ }
225
+
226
+ if (isLoopbackHost(parsed.host)) {
227
+ return context.wildcardWithoutReachableAddress ? 'no-reachable-ipv4' : 'loopback-endpoint';
228
+ }
229
+
230
+ if (isWildcardHost(parsed.host)) {
231
+ return 'wildcard-endpoint';
232
+ }
233
+
234
+ if (/^169\.254\./.test(parsed.host)) {
235
+ return 'link-local-endpoint';
236
+ }
237
+
238
+ return 'ok';
239
+ }
240
+
241
+ function safeText(value, maxLength = 1000) {
242
+ return String(value ?? '').replace(/\0/g, '').trim().slice(0, maxLength);
243
+ }
244
+
245
+ function normalizeApprovalLevel(value) {
246
+ const level = safeString(value, 80).toLowerCase();
247
+ return level === 'ai-assist' ? 'ai-assist' : 'task-only';
248
+ }
249
+
250
+ function normalizeRemoteInputEvent(value = {}) {
251
+ const input = value && typeof value === 'object' ? value : {};
252
+ const type = safeString(input.type || input.Type, 48);
253
+ const normalizedX = Number(input.normalizedX ?? input.NormalizedX);
254
+ const normalizedY = Number(input.normalizedY ?? input.NormalizedY);
255
+ const deltaX = Number(input.deltaX ?? input.DeltaX);
256
+ const deltaY = Number(input.deltaY ?? input.DeltaY);
257
+ return {
258
+ type,
259
+ normalizedX: Number.isFinite(normalizedX) ? Math.max(0, Math.min(1, normalizedX)) : undefined,
260
+ normalizedY: Number.isFinite(normalizedY) ? Math.max(0, Math.min(1, normalizedY)) : undefined,
261
+ button: safeString(input.button || input.Button, 24),
262
+ deltaX: Number.isFinite(deltaX) ? Math.max(-4096, Math.min(4096, deltaX)) : 0,
263
+ deltaY: Number.isFinite(deltaY) ? Math.max(-4096, Math.min(4096, deltaY)) : 0,
264
+ key: safeString(input.key || input.Key, 80),
265
+ code: safeString(input.code || input.Code, 80),
266
+ text: safeText(input.text || input.Text, 256),
267
+ repeat: input.repeat === true || input.Repeat === true,
268
+ controlLeaseId: safeString(input.controlLeaseId || input.ControlLeaseId, 128),
269
+ issuedAt: safeString(input.issuedAt || input.IssuedAt, 80)
270
+ };
271
+ }
272
+
273
+ function readCapabilityFlag(capabilities, key) {
274
+ const value = capabilities?.[key];
275
+ if (typeof value === 'boolean') {
276
+ return value;
277
+ }
278
+ if (typeof value === 'number') {
279
+ return value !== 0;
280
+ }
281
+ return /^(1|true|yes|on)$/i.test(String(value || '').trim());
282
+ }
283
+
284
+ function createDeviceCounters(overrides = {}) {
285
+ return {
286
+ messagesReceived: 0,
287
+ statusReceived: 0,
288
+ commandsSent: 0,
289
+ commandResultsReceived: 0,
290
+ thumbnailFramesReceived: 0,
291
+ thumbnailFramesDropped: 0,
292
+ liveFramesReceived: 0,
293
+ liveFramesDropped: 0,
294
+ liveStreamsStarted: 0,
295
+ liveStreamsStopped: 0,
296
+ tasksQueued: 0,
297
+ taskResultsReceived: 0,
298
+ taskResultsFailed: 0,
299
+ taskResultsTimedOut: 0,
300
+ ...overrides
301
+ };
302
+ }
303
+
304
+ function normalizeDeviceId(value) {
305
+ const id = safeString(value, 128).replace(/[^a-zA-Z0-9_.:-]/g, '-');
306
+ return id || crypto.randomUUID();
307
+ }
308
+
309
+ function maskToken(token) {
310
+ const value = String(token || '');
311
+ if (value.length <= 8) {
312
+ return value ? '****' : '';
313
+ }
314
+
315
+ return `${value.slice(0, 4)}...${value.slice(-4)}`;
316
+ }
317
+
318
+ function timingSafeStringEqual(left, right) {
319
+ const a = Buffer.from(String(left || ''), 'utf8');
320
+ const b = Buffer.from(String(right || ''), 'utf8');
321
+ if (a.length !== b.length) {
322
+ return false;
323
+ }
324
+
325
+ return crypto.timingSafeEqual(a, b);
326
+ }
327
+
328
+ function parseJsonLine(line) {
329
+ const text = String(line || '').trim();
330
+ if (!text) {
331
+ return null;
332
+ }
333
+
334
+ return JSON.parse(text);
335
+ }
336
+
337
+ function createFrameAccessToken() {
338
+ return crypto.randomBytes(18).toString('base64url');
339
+ }
340
+
341
+ function buildRemoteFramePath(deviceId, frameKind, frame) {
342
+ const token = safeString(frame?.accessToken, 128);
343
+ const frameSeq = Number(frame?.frameSeq);
344
+ if (!deviceId || !token || !Number.isFinite(frameSeq)) {
345
+ return '';
346
+ }
347
+
348
+ const endpoint = frameKind === 'thumbnail' ? 'thumbnail' : 'live/frame';
349
+ const params = new URLSearchParams({
350
+ format: 'binary',
351
+ seq: String(Math.floor(frameSeq)),
352
+ token
353
+ });
354
+ return `/api/remote/devices/${encodeURIComponent(deviceId)}/${endpoint}?${params.toString()}`;
355
+ }
356
+
357
+ function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
358
+ if (!frame) {
359
+ return null;
360
+ }
361
+
362
+ const {
363
+ payload,
364
+ accessToken,
365
+ dataUrl,
366
+ ...publicFrame
367
+ } = frame;
368
+
369
+ const framePath = buildRemoteFramePath(deviceId, frameKind, frame);
370
+ const serialized = {
371
+ ...publicFrame,
372
+ framePath,
373
+ frameUrl: framePath
374
+ };
375
+ if (options.includeDataUrl === true) {
376
+ serialized.dataUrl = dataUrl || buildFrameDataUrl(payload, '', publicFrame.mimeType || publicFrame.format || 'image/jpeg');
377
+ }
378
+
379
+ return serialized;
380
+ }
381
+
382
+ function getRecentFrameCache(device, frameKind) {
383
+ if (!device) {
384
+ return null;
385
+ }
386
+
387
+ if (!device.recentFramePayloads) {
388
+ device.recentFramePayloads = {
389
+ thumbnail: [],
390
+ live: []
391
+ };
392
+ }
393
+
394
+ const kind = frameKind === 'thumbnail' ? 'thumbnail' : 'live';
395
+ if (!Array.isArray(device.recentFramePayloads[kind])) {
396
+ device.recentFramePayloads[kind] = [];
397
+ }
398
+
399
+ return device.recentFramePayloads[kind];
400
+ }
401
+
402
+ function getRecentFrameCacheLimit(frameKind) {
403
+ return frameKind === 'thumbnail'
404
+ ? RECENT_THUMBNAIL_FRAME_CACHE_LIMIT
405
+ : RECENT_LIVE_FRAME_CACHE_LIMIT;
406
+ }
407
+
408
+ function getRecentFrameCacheMaxBytes(frameKind) {
409
+ return frameKind === 'thumbnail'
410
+ ? RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES
411
+ : RECENT_LIVE_FRAME_CACHE_MAX_BYTES;
412
+ }
413
+
414
+ function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
415
+ if (!Array.isArray(cache)) {
416
+ return;
417
+ }
418
+
419
+ const limit = getRecentFrameCacheLimit(frameKind);
420
+ const maxBytes = getRecentFrameCacheMaxBytes(frameKind);
421
+ let byteTotal = 0;
422
+ for (let index = cache.length - 1; index >= 0; index -= 1) {
423
+ const entry = cache[index];
424
+ const frame = entry?.frame;
425
+ if (!entry
426
+ || entry.expiresAt <= nowMs
427
+ || !frame
428
+ || !Buffer.isBuffer(frame.payload)
429
+ || !Number.isFinite(Number(frame.frameSeq))
430
+ || !safeString(frame.accessToken, 128)) {
431
+ cache.splice(index, 1);
432
+ }
433
+ }
434
+
435
+ for (let index = 0; index < cache.length; index += 1) {
436
+ const entry = cache[index];
437
+ const byteLength = Number(entry?.byteLength || entry?.frame?.payload?.length || 0) || 0;
438
+ byteTotal += byteLength;
439
+ if (index >= limit || byteTotal > maxBytes) {
440
+ cache.splice(index);
441
+ break;
442
+ }
443
+ }
444
+ }
445
+
446
+ function rememberRecentFramePayload(device, frameKind, frame) {
447
+ if (!device || !frame || !Buffer.isBuffer(frame.payload)) {
448
+ return;
449
+ }
450
+
451
+ const frameSeq = Number(frame.frameSeq);
452
+ const accessToken = safeString(frame.accessToken, 128);
453
+ if (!Number.isFinite(frameSeq) || !accessToken) {
454
+ return;
455
+ }
456
+
457
+ const kind = frameKind === 'thumbnail' ? 'thumbnail' : 'live';
458
+ const cache = getRecentFrameCache(device, kind);
459
+ if (!cache) {
460
+ return;
461
+ }
462
+
463
+ const nowMs = Date.now();
464
+ const normalizedSeq = Math.floor(frameSeq);
465
+ for (let index = cache.length - 1; index >= 0; index -= 1) {
466
+ const existing = cache[index]?.frame;
467
+ if (Number(existing?.frameSeq) === normalizedSeq
468
+ || safeString(existing?.accessToken, 128) === accessToken) {
469
+ cache.splice(index, 1);
470
+ }
471
+ }
472
+
473
+ cache.unshift({
474
+ frame,
475
+ byteLength: frame.payload.length,
476
+ cachedAt: nowMs,
477
+ expiresAt: nowMs + RECENT_FRAME_CACHE_TTL_MS
478
+ });
479
+ pruneRecentFrameCache(cache, kind, nowMs);
480
+ }
481
+
482
+ function isFramePayloadRequestMatch(frame, options = {}) {
483
+ if (!frame || !Buffer.isBuffer(frame.payload)) {
484
+ return false;
485
+ }
486
+
487
+ const requestedSeq = Number(options.frameSeq);
488
+ if (Number.isFinite(requestedSeq) && Math.floor(requestedSeq) !== Number(frame.frameSeq)) {
489
+ return false;
490
+ }
491
+
492
+ const token = safeString(options.token, 128);
493
+ if (token && !timingSafeStringEqual(token, frame.accessToken)) {
494
+ return false;
495
+ }
496
+
497
+ if (options.requireToken === true && !token) {
498
+ return false;
499
+ }
500
+
501
+ return true;
502
+ }
503
+
504
+ function findRecentFramePayload(device, frameKind, options = {}) {
505
+ const kind = frameKind === 'thumbnail' ? 'thumbnail' : 'live';
506
+ const latest = kind === 'thumbnail'
507
+ ? device?.latestThumbnail
508
+ : device?.latestLiveFrame;
509
+ if (isFramePayloadRequestMatch(latest, options)) {
510
+ return latest;
511
+ }
512
+
513
+ const token = safeString(options.token, 128);
514
+ const requestedSeq = Number(options.frameSeq);
515
+ if (!token && !Number.isFinite(requestedSeq)) {
516
+ return null;
517
+ }
518
+
519
+ const cache = getRecentFrameCache(device, kind);
520
+ if (!cache) {
521
+ return null;
522
+ }
523
+
524
+ pruneRecentFrameCache(cache, kind);
525
+ const entry = cache.find(item => isFramePayloadRequestMatch(item?.frame, options));
526
+ return entry?.frame || null;
527
+ }
528
+
529
+ function serializeDevice(device, options = {}) {
530
+ if (!device) {
531
+ return null;
532
+ }
533
+
534
+ return {
535
+ deviceId: device.deviceId,
536
+ sessionId: device.sessionId,
537
+ deviceName: device.deviceName,
538
+ hostname: device.hostname,
539
+ platform: device.platform,
540
+ arch: device.arch,
541
+ pid: device.pid,
542
+ agentVersion: device.agentVersion,
543
+ capabilities: { ...device.capabilities },
544
+ connected: device.connected,
545
+ connectedAt: device.connectedAt,
546
+ disconnectedAt: device.disconnectedAt,
547
+ lastSeenAt: device.lastSeenAt,
548
+ lastStatusAt: device.lastStatusAt,
549
+ lastDisconnectReason: device.lastDisconnectReason,
550
+ remoteAddress: device.remoteAddress,
551
+ remotePort: device.remotePort,
552
+ latestThumbnail: serializeRemoteFrame(device.latestThumbnail, device.deviceId, 'thumbnail', options),
553
+ latestLiveFrame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', options),
554
+ activeLiveStream: device.activeLiveStream ? { ...device.activeLiveStream } : null,
555
+ latestTask: device.latestTask ? { ...device.latestTask } : null,
556
+ synthetic: device.synthetic === true,
557
+ recentTasks: Array.isArray(device.recentTasks)
558
+ ? device.recentTasks.map(task => ({ ...task }))
559
+ : [],
560
+ status: { ...device.status },
561
+ counters: { ...device.counters }
562
+ };
563
+ }
564
+
565
+ function serializeTaskBatch(batch) {
566
+ if (!batch) {
567
+ return null;
568
+ }
569
+
570
+ return {
571
+ batchId: batch.batchId,
572
+ title: batch.title,
573
+ instructionPreview: batch.instructionPreview,
574
+ approvalLevel: batch.approvalLevel,
575
+ status: batch.status,
576
+ requestedAt: batch.requestedAt,
577
+ updatedAt: batch.updatedAt,
578
+ total: batch.total,
579
+ queued: batch.queued,
580
+ pending: batch.pending,
581
+ completed: batch.completed,
582
+ failed: batch.failed,
583
+ timedOut: batch.timedOut,
584
+ results: Array.isArray(batch.results)
585
+ ? batch.results.map(item => ({ ...item }))
586
+ : []
587
+ };
588
+ }
589
+
590
+ function writeJsonLine(socket, payload) {
591
+ if (!socket || socket.destroyed) {
592
+ return false;
593
+ }
594
+
595
+ if (socket.__remoteHubWebSocket === true && typeof socket.sendJson === 'function') {
596
+ return socket.sendJson(payload);
597
+ }
598
+
599
+ socket.write(`${JSON.stringify(payload)}\n`);
600
+ return true;
601
+ }
602
+
603
+ function isRemoteHubWebSocketUpgradeStart(buffer) {
604
+ if (!Buffer.isBuffer(buffer) || buffer.length < 4) {
605
+ return false;
606
+ }
607
+
608
+ return buffer.subarray(0, Math.min(buffer.length, 16)).toString('latin1').startsWith('GET ');
609
+ }
610
+
611
+ function parseRemoteHubWebSocketUpgrade(buffer) {
612
+ const headerEnd = buffer.indexOf('\r\n\r\n');
613
+ if (headerEnd < 0) {
614
+ return null;
615
+ }
616
+
617
+ const headerText = buffer.subarray(0, headerEnd).toString('latin1');
618
+ const lines = headerText.split('\r\n');
619
+ const requestLine = lines.shift() || '';
620
+ const [method, path] = requestLine.split(/\s+/);
621
+ const headers = {};
622
+ for (const line of lines) {
623
+ const separator = line.indexOf(':');
624
+ if (separator <= 0) {
625
+ continue;
626
+ }
627
+
628
+ headers[line.slice(0, separator).trim().toLowerCase()] = line.slice(separator + 1).trim();
629
+ }
630
+
631
+ return {
632
+ method,
633
+ path,
634
+ headers,
635
+ head: buffer.subarray(headerEnd + 4)
636
+ };
637
+ }
638
+
639
+ function buildRemoteHubWebSocketAcceptKey(key) {
640
+ return crypto
641
+ .createHash('sha1')
642
+ .update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
643
+ .digest('base64');
644
+ }
645
+
646
+ function buildRemoteHubWebSocketFrame(opcode, payload) {
647
+ const body = Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload || ''), 'utf8');
648
+ let header = null;
649
+ if (body.length < 126) {
650
+ header = Buffer.allocUnsafe(2);
651
+ header[0] = 0x80 | opcode;
652
+ header[1] = body.length;
653
+ } else if (body.length <= 0xffff) {
654
+ header = Buffer.allocUnsafe(4);
655
+ header[0] = 0x80 | opcode;
656
+ header[1] = 126;
657
+ header.writeUInt16BE(body.length, 2);
658
+ } else {
659
+ header = Buffer.allocUnsafe(10);
660
+ header[0] = 0x80 | opcode;
661
+ header[1] = 127;
662
+ header.writeBigUInt64BE(BigInt(body.length), 2);
663
+ }
664
+
665
+ return Buffer.concat([header, body], header.length + body.length);
666
+ }
667
+
668
+ function parseRemoteHubWebSocketBinaryFrame(buffer) {
669
+ if (!Buffer.isBuffer(buffer) || buffer.length < 5) {
670
+ return null;
671
+ }
672
+
673
+ const metaLength = buffer.readUInt32BE(0);
674
+ if (!Number.isFinite(metaLength)
675
+ || metaLength <= 0
676
+ || metaLength > 64 * 1024
677
+ || 4 + metaLength >= buffer.length) {
678
+ return null;
679
+ }
680
+
681
+ const header = JSON.parse(buffer.subarray(4, 4 + metaLength).toString('utf8'));
682
+ const payload = buffer.subarray(4 + metaLength);
683
+ return { header, payload };
684
+ }
685
+
686
+ class RemoteHubWebSocketAgentSocket {
687
+ constructor(socket) {
688
+ this.__remoteHubWebSocket = true;
689
+ this.socket = socket;
690
+ this.remoteAddress = socket.remoteAddress;
691
+ this.remotePort = socket.remotePort;
692
+ this.destroyed = false;
693
+ this.buffer = Buffer.alloc(0);
694
+ this.fragmentOpcode = 0;
695
+ this.fragments = [];
696
+ this.onTextMessage = () => {};
697
+ this.onBinaryMessage = () => {};
698
+ this.onCloseMessage = () => {};
699
+ }
700
+
701
+ setNoDelay() {}
702
+
703
+ setKeepAlive() {}
704
+
705
+ sendJson(payload) {
706
+ return this.sendText(JSON.stringify(payload));
707
+ }
708
+
709
+ write(payload) {
710
+ if (this.destroyed) {
711
+ return false;
712
+ }
713
+
714
+ const text = (Buffer.isBuffer(payload) ? payload.toString('utf8') : String(payload || '')).replace(/\n$/, '');
715
+ return this.sendText(text);
716
+ }
717
+
718
+ sendText(text) {
719
+ return this.sendFrame(0x1, Buffer.from(String(text || ''), 'utf8'));
720
+ }
721
+
722
+ sendFrame(opcode, payload) {
723
+ if (this.destroyed || this.socket.destroyed) {
724
+ return false;
725
+ }
726
+
727
+ try {
728
+ this.socket.write(buildRemoteHubWebSocketFrame(opcode, payload));
729
+ return true;
730
+ } catch {
731
+ this.destroy();
732
+ return false;
733
+ }
734
+ }
735
+
736
+ destroy() {
737
+ if (this.destroyed) {
738
+ return;
739
+ }
740
+
741
+ this.destroyed = true;
742
+ try {
743
+ if (!this.socket.destroyed) {
744
+ this.socket.write(buildRemoteHubWebSocketFrame(0x8, Buffer.alloc(0)));
745
+ }
746
+ } catch {
747
+ // Ignore close-frame failures.
748
+ }
749
+ try {
750
+ this.socket.destroy();
751
+ } catch {
752
+ // Ignore socket destroy failures.
753
+ }
754
+ }
755
+
756
+ handleData(chunk) {
757
+ if (this.destroyed) {
758
+ return;
759
+ }
760
+
761
+ this.buffer = this.buffer.length > 0
762
+ ? Buffer.concat([this.buffer, chunk])
763
+ : chunk;
764
+
765
+ while (!this.destroyed) {
766
+ if (this.buffer.length < 2) {
767
+ return;
768
+ }
769
+
770
+ const first = this.buffer[0];
771
+ const second = this.buffer[1];
772
+ const fin = (first & 0x80) !== 0;
773
+ const opcode = first & 0x0f;
774
+ const masked = (second & 0x80) !== 0;
775
+ let payloadLength = second & 0x7f;
776
+ let offset = 2;
777
+
778
+ if (payloadLength === 126) {
779
+ if (this.buffer.length < offset + 2) return;
780
+ payloadLength = this.buffer.readUInt16BE(offset);
781
+ offset += 2;
782
+ } else if (payloadLength === 127) {
783
+ if (this.buffer.length < offset + 8) return;
784
+ const bigLength = this.buffer.readBigUInt64BE(offset);
785
+ if (bigLength > BigInt(Number.MAX_SAFE_INTEGER)) {
786
+ this.destroy();
787
+ return;
788
+ }
789
+ payloadLength = Number(bigLength);
790
+ offset += 8;
791
+ }
792
+
793
+ if (!masked) {
794
+ this.destroy();
795
+ return;
796
+ }
797
+
798
+ if (this.buffer.length < offset + 4 + payloadLength) {
799
+ return;
800
+ }
801
+
802
+ const mask = this.buffer.subarray(offset, offset + 4);
803
+ offset += 4;
804
+ const payload = Buffer.from(this.buffer.subarray(offset, offset + payloadLength));
805
+ this.buffer = this.buffer.subarray(offset + payloadLength);
806
+ for (let index = 0; index < payload.length; index += 1) {
807
+ payload[index] ^= mask[index & 3];
808
+ }
809
+
810
+ if (opcode === 0x8) {
811
+ this.destroyed = true;
812
+ this.onCloseMessage('websocket-close');
813
+ try {
814
+ this.socket.destroy();
815
+ } catch {
816
+ // Ignore socket destroy failures.
817
+ }
818
+ return;
819
+ }
820
+
821
+ if (opcode === 0x9) {
822
+ this.sendFrame(0xA, payload);
823
+ continue;
824
+ }
825
+
826
+ if (opcode === 0xA) {
827
+ continue;
828
+ }
829
+
830
+ let messageOpcode = opcode;
831
+ let messagePayload = payload;
832
+ if (!fin) {
833
+ this.fragmentOpcode = opcode;
834
+ this.fragments = [payload];
835
+ continue;
836
+ }
837
+
838
+ if (opcode === 0x0) {
839
+ this.fragments.push(payload);
840
+ messageOpcode = this.fragmentOpcode;
841
+ messagePayload = Buffer.concat(this.fragments);
842
+ this.fragmentOpcode = 0;
843
+ this.fragments = [];
844
+ }
845
+
846
+ if (messageOpcode === 0x1) {
847
+ this.onTextMessage(messagePayload.toString('utf8'));
848
+ } else if (messageOpcode === 0x2) {
849
+ this.onBinaryMessage(messagePayload);
850
+ }
851
+ }
852
+ }
853
+ }
854
+
855
+ export function createRemoteHub(options = {}) {
856
+ const env = options.env || process.env;
857
+ const logEvent = options.logEvent || (() => {});
858
+ const logWarn = options.logWarn || (() => {});
859
+ const emitEvent = options.emitEvent || (() => {});
860
+ const emitFrame = typeof options.emitFrame === 'function' ? options.emitFrame : (() => {});
861
+
862
+ const enabled = isEnabledValue(env.MINDEXEC_REMOTE_HUB ?? env.REMOTE_HUB_ENABLED, true)
863
+ && !isDisabledValue(env.REMOTE_HUB_DISABLED);
864
+ const host = safeString(env.REMOTE_HUB_HOST || DEFAULT_REMOTE_HUB_HOST, 128);
865
+ const requestedPort = normalizePort(env.REMOTE_HUB_PORT || DEFAULT_REMOTE_HUB_PORT);
866
+ const heartbeatMs = clampNumber(env.REMOTE_HUB_HEARTBEAT_MS, 1000, 60000, DEFAULT_HEARTBEAT_MS);
867
+ const taskTimeoutMs = clampNumber(
868
+ options.taskTimeoutMs ?? env.REMOTE_HUB_TASK_TIMEOUT_MS,
869
+ 50,
870
+ 30 * 60 * 1000,
871
+ DEFAULT_AGENT_TASK_TIMEOUT_MS);
872
+ const managerPackage = safeString(options.managerPackage ?? env.MINDEXEC_MANAGER_PACKAGE ?? 'mindexec-ai', 128);
873
+ const managerVersion = safeString(options.managerVersion ?? env.MINDEXEC_MANAGER_VERSION ?? '', 64);
874
+ const hostInstanceId = safeString(options.hostInstanceId ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
875
+ const publicEndpoint = safeString(env.MINDEXEC_REMOTE_PUBLIC_ENDPOINT || env.REMOTE_HUB_PUBLIC_ENDPOINT, 256);
876
+ const publicHost = safeString(env.MINDEXEC_REMOTE_PUBLIC_HOST || env.REMOTE_HUB_PUBLIC_HOST, 128);
877
+ const pairToken = safeString(
878
+ options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN || crypto.randomBytes(6).toString('hex'),
879
+ 256);
880
+
881
+ const devices = new Map();
882
+ const taskBatches = new Map();
883
+ const sockets = new Map();
884
+ const allSockets = new Set();
885
+ let server = null;
886
+ let started = false;
887
+ let boundPort = requestedPort;
888
+ let lastError = '';
889
+ let hostTarget = null;
890
+
891
+ function getAnnouncedHost() {
892
+ if (publicHost) {
893
+ return publicHost;
894
+ }
895
+
896
+ if (isWildcardHost(host)) {
897
+ return getReachableIPv4Address();
898
+ }
899
+
900
+ return host;
901
+ }
902
+
903
+ function getAgentEndpoint() {
904
+ if (publicEndpoint) {
905
+ return publicEndpoint;
906
+ }
907
+
908
+ return `${getAnnouncedHost()}:${boundPort || requestedPort}`;
909
+ }
910
+
911
+ function getAgentEndpointRouteInfo() {
912
+ const includeInterfaceCandidates = !publicEndpoint && isWildcardHost(host);
913
+ const candidateDetails = includeInterfaceCandidates
914
+ ? getReachableIPv4CandidateEntries()
915
+ : [];
916
+ const candidateEndpoints = candidateDetails
917
+ .map(candidate => ({
918
+ ...candidate,
919
+ endpoint: buildEndpoint(candidate.address, boundPort || requestedPort)
920
+ }))
921
+ .filter(candidate => candidate.endpoint);
922
+ const endpoint = getAgentEndpoint();
923
+ const endpointCandidates = [
924
+ endpoint,
925
+ ...candidateEndpoints.map(candidate => candidate.endpoint)
926
+ ].filter((value, index, all) => value && all.indexOf(value) === index);
927
+ const wildcardWithoutReachableAddress =
928
+ !publicEndpoint
929
+ && !publicHost
930
+ && isWildcardHost(host)
931
+ && candidateDetails.length === 0;
932
+ const reason = getAccountRouteEndpointReason(endpoint, { wildcardWithoutReachableAddress });
933
+ const parsed = parseManagerEndpoint(endpoint);
934
+ return {
935
+ endpoint,
936
+ host: parsed?.host || '',
937
+ port: parsed?.port || 0,
938
+ accountRouteReady: reason === 'ok',
939
+ reason,
940
+ candidates: endpointCandidates,
941
+ candidateDetails: candidateEndpoints
942
+ };
943
+ }
944
+
945
+ function getActiveHostTarget() {
946
+ if (!hostTarget) {
947
+ return null;
948
+ }
949
+
950
+ const expiresMs = Date.parse(hostTarget.expiresAt || '');
951
+ if (Number.isFinite(expiresMs) && expiresMs <= Date.now()) {
952
+ const expiredTarget = hostTarget;
953
+ hostTarget = null;
954
+ emitRemoteEvent('RemoteHostTargetExpired', null, {
955
+ hostTarget: {
956
+ ...expiredTarget,
957
+ active: false
958
+ }
959
+ });
960
+ return null;
961
+ }
962
+
963
+ return hostTarget;
964
+ }
965
+
966
+ function serializeHostTarget(target = getActiveHostTarget()) {
967
+ if (!target) {
968
+ return {
969
+ active: false,
970
+ nodeId: '',
971
+ leaseId: '',
972
+ hostInstanceId: '',
973
+ endpoint: '',
974
+ endpointCandidates: [],
975
+ activatedAt: '',
976
+ updatedAt: '',
977
+ expiresAt: ''
978
+ };
979
+ }
980
+
981
+ return {
982
+ active: true,
983
+ nodeId: target.nodeId,
984
+ leaseId: target.leaseId,
985
+ hostInstanceId: target.hostInstanceId || hostInstanceId,
986
+ endpoint: target.endpoint,
987
+ endpointCandidates: Array.isArray(target.endpointCandidates) ? [...target.endpointCandidates] : [target.endpoint].filter(Boolean),
988
+ activatedAt: target.activatedAt,
989
+ updatedAt: target.updatedAt,
990
+ expiresAt: target.expiresAt
991
+ };
992
+ }
993
+
994
+ function setHostTarget(options = {}) {
995
+ const enabled = options.enabled !== false;
996
+ const nodeId = safeString(options.nodeId, 128);
997
+ if (!enabled) {
998
+ const previous = getActiveHostTarget();
999
+ if (!previous) {
1000
+ return {
1001
+ ok: true,
1002
+ active: false,
1003
+ hostTarget: serializeHostTarget(null)
1004
+ };
1005
+ }
1006
+
1007
+ if (nodeId && previous.nodeId && nodeId !== previous.nodeId) {
1008
+ return {
1009
+ ok: false,
1010
+ error: 'host-target-node-mismatch',
1011
+ active: true,
1012
+ hostTarget: serializeHostTarget(previous)
1013
+ };
1014
+ }
1015
+
1016
+ hostTarget = null;
1017
+ emitRemoteEvent('RemoteHostTargetCleared', null, {
1018
+ hostTarget: {
1019
+ ...serializeHostTarget(previous),
1020
+ active: false
1021
+ }
1022
+ });
1023
+ return {
1024
+ ok: true,
1025
+ active: false,
1026
+ hostTarget: serializeHostTarget(null)
1027
+ };
1028
+ }
1029
+
1030
+ if (!nodeId) {
1031
+ return {
1032
+ ok: false,
1033
+ error: 'missing-node-id',
1034
+ active: false,
1035
+ hostTarget: serializeHostTarget()
1036
+ };
1037
+ }
1038
+
1039
+ const now = new Date();
1040
+ const leaseMs = clampNumber(options.leaseMs, 5000, 10 * 60 * 1000, DEFAULT_HOST_TARGET_LEASE_MS);
1041
+ const previous = getActiveHostTarget();
1042
+ const activeSameNode = previous?.nodeId === nodeId;
1043
+ const routeInfo = getAgentEndpointRouteInfo();
1044
+ hostTarget = {
1045
+ nodeId,
1046
+ leaseId: activeSameNode && previous?.leaseId
1047
+ ? previous.leaseId
1048
+ : (safeString(options.leaseId, 128) || crypto.randomUUID()),
1049
+ hostInstanceId,
1050
+ endpoint: routeInfo.endpoint,
1051
+ endpointCandidates: routeInfo.candidates,
1052
+ activatedAt: activeSameNode && previous?.activatedAt ? previous.activatedAt : now.toISOString(),
1053
+ updatedAt: now.toISOString(),
1054
+ expiresAt: new Date(now.getTime() + leaseMs).toISOString()
1055
+ };
1056
+
1057
+ emitRemoteEvent('RemoteHostTargetSet', null, {
1058
+ hostTarget: serializeHostTarget(hostTarget),
1059
+ replacedNodeId: previous && previous.nodeId !== nodeId ? previous.nodeId : ''
1060
+ });
1061
+ if (!activeSameNode || !routeInfo.accountRouteReady) {
1062
+ logEvent(
1063
+ 'remote',
1064
+ `host target ${routeInfo.accountRouteReady ? 'account-ready' : 'local-only'} endpoint=${routeInfo.endpoint} candidates=${routeInfo.candidates.length} reason=${routeInfo.reason}`,
1065
+ 'remote');
1066
+ }
1067
+ return {
1068
+ ok: true,
1069
+ active: true,
1070
+ hostTarget: serializeHostTarget(hostTarget)
1071
+ };
1072
+ }
1073
+
1074
+ function getStatus({ includeSecrets = false } = {}) {
1075
+ const connectedDevices = [...devices.values()].filter(device => device.connected).length;
1076
+ const activeHostTarget = serializeHostTarget();
1077
+ const routeInfo = getAgentEndpointRouteInfo();
1078
+ return {
1079
+ enabled,
1080
+ started,
1081
+ host,
1082
+ agentHost: routeInfo.host || getAnnouncedHost(),
1083
+ port: boundPort || requestedPort,
1084
+ protocol: 'tcp-jsonl',
1085
+ protocolVersion: REMOTE_PROTOCOL_VERSION,
1086
+ heartbeatMs,
1087
+ taskTimeoutMs,
1088
+ managerPackage,
1089
+ managerVersion,
1090
+ hostInstanceId,
1091
+ agentPackage: '@mindexec/remote',
1092
+ agentEndpoint: routeInfo.endpoint,
1093
+ agentEndpointAccountRouteReady: routeInfo.accountRouteReady,
1094
+ agentEndpointRouteReason: routeInfo.reason,
1095
+ agentEndpointCandidates: routeInfo.candidates,
1096
+ agentEndpointCandidateDetails: routeInfo.candidateDetails,
1097
+ pairToken: includeSecrets ? pairToken : undefined,
1098
+ pairTokenPreview: maskToken(pairToken),
1099
+ deviceCount: devices.size,
1100
+ connectedDeviceCount: connectedDevices,
1101
+ canvasDeviceListMode: 'all-devices',
1102
+ canvasPagination: 'none',
1103
+ hostTargetActive: activeHostTarget.active,
1104
+ hostTargetNodeId: activeHostTarget.nodeId,
1105
+ hostTargetLeaseId: activeHostTarget.leaseId,
1106
+ hostTargetHostInstanceId: activeHostTarget.hostInstanceId,
1107
+ hostTargetEndpoint: activeHostTarget.endpoint,
1108
+ hostTargetEndpointCandidates: activeHostTarget.endpointCandidates,
1109
+ hostTargetActivatedAt: activeHostTarget.activatedAt,
1110
+ hostTargetUpdatedAt: activeHostTarget.updatedAt,
1111
+ hostTargetExpiresAt: activeHostTarget.expiresAt,
1112
+ externalExposure: isWildcardHost(host) || !isLoopbackHost(host),
1113
+ lastError
1114
+ };
1115
+ }
1116
+
1117
+ function listDevices(options = {}) {
1118
+ const serializeOptions = {
1119
+ includeDataUrl: options.includeDataUrl === true
1120
+ };
1121
+ return [...devices.values()]
1122
+ .map(device => serializeDevice(device, serializeOptions))
1123
+ .filter(Boolean)
1124
+ .sort((a, b) => {
1125
+ const nameCompare = String(a.deviceName || '').localeCompare(String(b.deviceName || ''));
1126
+ if (nameCompare !== 0) {
1127
+ return nameCompare;
1128
+ }
1129
+
1130
+ return String(a.deviceId).localeCompare(String(b.deviceId));
1131
+ });
1132
+ }
1133
+
1134
+ function listDeviceFrames(options = {}) {
1135
+ const ids = Array.isArray(options.deviceIds)
1136
+ ? options.deviceIds.map(value => safeString(value, 160)).filter(Boolean)
1137
+ : [];
1138
+ const idSet = ids.length > 0 ? new Set(ids) : null;
1139
+ const includeThumbnail = options.includeThumbnail !== false;
1140
+ const includeLive = options.includeLive !== false;
1141
+
1142
+ return [...devices.values()]
1143
+ .filter(device => !idSet || idSet.has(device.deviceId))
1144
+ .map(device => ({
1145
+ deviceId: device.deviceId,
1146
+ connected: device.connected === true,
1147
+ thumbnailEnabled: readCapabilityFlag(device.capabilities, 'thumbnail'),
1148
+ liveStreamActive: device.activeLiveStream?.active === true,
1149
+ liveStreamId: device.activeLiveStream?.streamId || '',
1150
+ liveStreamFps: Number(device.activeLiveStream?.fps || 0),
1151
+ liveStreamLastFrameAt: device.activeLiveStream?.lastFrameAt || '',
1152
+ thumbnail: includeThumbnail
1153
+ ? serializeRemoteFrame(device.latestThumbnail, device.deviceId, 'thumbnail', { includeDataUrl: false })
1154
+ : null,
1155
+ live: includeLive
1156
+ ? serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', { includeDataUrl: false })
1157
+ : null
1158
+ }))
1159
+ .sort((a, b) => String(a.deviceId).localeCompare(String(b.deviceId)));
1160
+ }
1161
+
1162
+ function listTaskBatches() {
1163
+ return [...taskBatches.values()]
1164
+ .map(serializeTaskBatch)
1165
+ .filter(Boolean)
1166
+ .sort((left, right) => String(right.requestedAt || '').localeCompare(String(left.requestedAt || '')));
1167
+ }
1168
+
1169
+ function getLatestTaskBatch() {
1170
+ return listTaskBatches()[0] || null;
1171
+ }
1172
+
1173
+ function emitRemoteEvent(type, device = null, extra = {}) {
1174
+ emitEvent(type, {
1175
+ ...extra,
1176
+ remoteHub: getStatus({ includeSecrets: false }),
1177
+ device: serializeDevice(device)
1178
+ });
1179
+ }
1180
+
1181
+ function makeSyntheticFrame(device, streamId, mode = 'thumbnail', options = {}) {
1182
+ const now = new Date().toISOString();
1183
+ const frameSeq = Number(device?.counters?.thumbnailFramesReceived || 0)
1184
+ + Number(device?.counters?.liveFramesReceived || 0)
1185
+ + 1;
1186
+ return {
1187
+ streamId: safeString(streamId, 128) || `${mode}-${Date.now()}`,
1188
+ frameSeq,
1189
+ commandId: safeString(options.commandId, 128),
1190
+ width: clampNumber(options.width, 2, 2560, mode === 'remote-fast' ? 960 : 360),
1191
+ height: clampNumber(options.height, 1, 1440, mode === 'remote-fast' ? 540 : 220),
1192
+ mimeType: 'image/png',
1193
+ format: 'image/png',
1194
+ mode,
1195
+ fps: clampNumber(options.fps, 1, 24, mode === 'remote-fast' ? 20 : 1),
1196
+ capturedAt: now,
1197
+ receivedAt: now,
1198
+ byteLength: 68,
1199
+ transport: 'synthetic',
1200
+ contentHash: SYNTHETIC_FRAME_HASH,
1201
+ captureMs: 0,
1202
+ sameContentStreak: 0,
1203
+ dataUrl: SYNTHETIC_FRAME_DATA_URL,
1204
+ payload: SYNTHETIC_FRAME_PAYLOAD,
1205
+ accessToken: createFrameAccessToken()
1206
+ };
1207
+ }
1208
+
1209
+ function applySyntheticThumbnail(device, command) {
1210
+ if (!device) {
1211
+ return null;
1212
+ }
1213
+
1214
+ const payload = command?.payload || {};
1215
+ const frame = makeSyntheticFrame(device, payload.streamId || 'synthetic-thumb', 'thumbnail', {
1216
+ commandId: command?.commandId,
1217
+ width: payload.maxWidth || 360,
1218
+ height: payload.maxHeight || 220
1219
+ });
1220
+ delete frame.mode;
1221
+ delete frame.fps;
1222
+ device.latestThumbnail = frame;
1223
+ rememberRecentFramePayload(device, 'thumbnail', frame);
1224
+ device.lastSeenAt = frame.receivedAt;
1225
+ device.counters.thumbnailFramesReceived += 1;
1226
+ emitRemoteEvent('RemoteFrameReceived', device, {
1227
+ streamId: frame.streamId,
1228
+ frameSeq: frame.frameSeq,
1229
+ synthetic: true
1230
+ });
1231
+ return frame;
1232
+ }
1233
+
1234
+ function applySyntheticLiveFrame(device, streamId, options = {}) {
1235
+ if (!device?.activeLiveStream?.active) {
1236
+ return null;
1237
+ }
1238
+
1239
+ const frame = makeSyntheticFrame(device, streamId || device.activeLiveStream.streamId, 'remote-fast', {
1240
+ commandId: options.commandId || device.activeLiveStream.commandId,
1241
+ width: options.maxWidth || 960,
1242
+ height: options.maxHeight || 540,
1243
+ fps: options.fps || device.activeLiveStream.fps
1244
+ });
1245
+ device.latestLiveFrame = frame;
1246
+ rememberRecentFramePayload(device, 'live', frame);
1247
+ emitFrame({
1248
+ kind: 'live',
1249
+ deviceId: device.deviceId,
1250
+ frame: serializeRemoteFrame(frame, device.deviceId, 'live', { includeDataUrl: false }),
1251
+ payload: frame.payload,
1252
+ mimeType: frame.mimeType,
1253
+ byteLength: frame.byteLength
1254
+ });
1255
+ device.activeLiveStream.lastFrameAt = frame.receivedAt;
1256
+ device.activeLiveStream.lastFrameSeq = frame.frameSeq;
1257
+ device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
1258
+ device.lastSeenAt = frame.receivedAt;
1259
+ device.counters.liveFramesReceived += 1;
1260
+ emitRemoteEvent('RemoteFrameReceived', device, {
1261
+ streamId: frame.streamId,
1262
+ frameSeq: frame.frameSeq,
1263
+ synthetic: true
1264
+ });
1265
+ return frame;
1266
+ }
1267
+
1268
+ function closeExistingDeviceSocket(deviceId, nextSessionId) {
1269
+ const existing = devices.get(deviceId);
1270
+ if (!existing?.socket || existing.socket.destroyed) {
1271
+ return;
1272
+ }
1273
+
1274
+ existing.lastDisconnectReason = 'replaced-by-new-session';
1275
+ failAllPendingTasks(existing, 'replaced-by-new-session');
1276
+ writeJsonLine(existing.socket, {
1277
+ type: 'disconnect',
1278
+ reason: 'replaced-by-new-session',
1279
+ nextSessionId
1280
+ });
1281
+ existing.socket.destroy();
1282
+ }
1283
+
1284
+ function clearSyntheticFleet() {
1285
+ let removed = 0;
1286
+ for (const [deviceId, device] of devices.entries()) {
1287
+ if (device.synthetic === true) {
1288
+ devices.delete(deviceId);
1289
+ removed += 1;
1290
+ }
1291
+ }
1292
+
1293
+ return { ok: true, removed, total: devices.size };
1294
+ }
1295
+
1296
+ function seedSyntheticFleet(options = {}) {
1297
+ const count = clampNumber(options.count, 1, MAX_SYNTHETIC_DEVICES, 250);
1298
+ const connectedRatio = Math.max(0, Math.min(1, Number(options.connectedRatio ?? 0.88)));
1299
+ const thumbnailRatio = Math.max(0, Math.min(1, Number(options.thumbnailRatio ?? 0.7)));
1300
+ const aiAssistRatio = Math.max(0, Math.min(1, Number(options.aiAssistRatio ?? 0.35)));
1301
+ const liveCount = clampNumber(options.liveCount, 0, Math.min(count, 24), Math.min(6, count));
1302
+ const replace = options.replace !== false;
1303
+ const now = new Date();
1304
+ const platforms = ['win32', 'linux', 'darwin'];
1305
+
1306
+ if (replace) {
1307
+ clearSyntheticFleet();
1308
+ }
1309
+
1310
+ let connected = 0;
1311
+ let aiAssist = 0;
1312
+ let live = 0;
1313
+ for (let index = 0; index < count; index += 1) {
1314
+ const ordinal = index + 1;
1315
+ const deviceId = `synthetic-${String(ordinal).padStart(4, '0')}`;
1316
+ const isConnected = index / count < connectedRatio;
1317
+ const hasThumbnail = index / count < thumbnailRatio;
1318
+ const hasAiAssist = index / count < aiAssistRatio;
1319
+ const isLive = isConnected && index < liveCount;
1320
+ const seenAt = new Date(now.getTime() - index * 1350).toISOString();
1321
+ const connectedAt = new Date(now.getTime() - (index + 8) * 60000).toISOString();
1322
+ const platform = platforms[index % platforms.length];
1323
+ const usedMemRatio = Number((0.18 + (index % 71) / 100).toFixed(2));
1324
+ const load1 = Number(((index % 19) / 3).toFixed(2));
1325
+ const taskStatus = index % 17 === 0
1326
+ ? 'failed'
1327
+ : index % 5 === 0
1328
+ ? 'completed'
1329
+ : index % 7 === 0
1330
+ ? 'queued'
1331
+ : '';
1332
+ const latestTask = taskStatus
1333
+ ? {
1334
+ taskId: `synthetic-task-${ordinal}`,
1335
+ commandId: `synthetic-command-${ordinal}`,
1336
+ title: taskStatus === 'failed' ? 'Synthetic issue check' : 'Synthetic status task',
1337
+ instructionPreview: 'Synthetic fleet scale validation task.',
1338
+ status: taskStatus,
1339
+ approvalLevel: hasAiAssist ? 'ai-assist' : 'task-only',
1340
+ requestedAt: seenAt,
1341
+ sentAt: seenAt,
1342
+ updatedAt: seenAt,
1343
+ completedAt: taskStatus === 'completed' || taskStatus === 'failed' ? seenAt : '',
1344
+ error: taskStatus === 'failed' ? 'Synthetic task failure sample.' : '',
1345
+ resultKind: 'synthetic-agent-task',
1346
+ resultModel: hasAiAssist ? 'synthetic-ai' : '',
1347
+ resultResponseId: hasAiAssist ? `synthetic-response-${ordinal}` : '',
1348
+ resultSummary: taskStatus === 'failed'
1349
+ ? 'Synthetic task reported a sample issue.'
1350
+ : 'Synthetic task completed for scale validation.'
1351
+ }
1352
+ : null;
1353
+ const device = {
1354
+ socket: null,
1355
+ synthetic: true,
1356
+ deviceId,
1357
+ sessionId: `synthetic-session-${ordinal}`,
1358
+ deviceName: `Synthetic PC ${String(ordinal).padStart(4, '0')}`,
1359
+ hostname: `synthetic-host-${String(ordinal).padStart(4, '0')}`,
1360
+ platform,
1361
+ arch: index % 4 === 0 ? 'arm64' : 'x64',
1362
+ pid: 0,
1363
+ agentVersion: 'synthetic-scale',
1364
+ capabilities: {
1365
+ status: true,
1366
+ thumbnail: hasThumbnail,
1367
+ control: false,
1368
+ liveStream: true,
1369
+ computerAgent: true,
1370
+ taskDispatch: true,
1371
+ aiAssist: hasAiAssist,
1372
+ aiModel: hasAiAssist ? 'synthetic-ai' : '',
1373
+ aiProvider: hasAiAssist ? 'synthetic' : ''
1374
+ },
1375
+ connected: isConnected,
1376
+ connectedAt: isConnected ? connectedAt : '',
1377
+ disconnectedAt: isConnected ? '' : seenAt,
1378
+ lastSeenAt: seenAt,
1379
+ lastStatusAt: seenAt,
1380
+ lastDisconnectReason: isConnected ? '' : 'synthetic-offline',
1381
+ remoteAddress: 'synthetic.local',
1382
+ remotePort: 0,
1383
+ status: {
1384
+ platform,
1385
+ release: platform === 'win32' ? 'Windows 11' : platform === 'darwin' ? 'macOS' : 'Linux',
1386
+ uptimeSec: (ordinal * 791) % 1209600,
1387
+ usedMemRatio,
1388
+ loadavg: [load1, Math.max(0, load1 - 0.2), Math.max(0, load1 - 0.4)]
1389
+ },
1390
+ latestThumbnail: null,
1391
+ latestLiveFrame: null,
1392
+ recentFramePayloads: {
1393
+ thumbnail: [],
1394
+ live: []
1395
+ },
1396
+ activeLiveStream: null,
1397
+ latestTask,
1398
+ recentTasks: latestTask ? [latestTask] : [],
1399
+ pendingTaskCommands: new Map(),
1400
+ pendingTaskTimers: new Map(),
1401
+ counters: createDeviceCounters({
1402
+ messagesReceived: 1,
1403
+ statusReceived: 1,
1404
+ tasksQueued: latestTask ? 1 : 0,
1405
+ taskResultsReceived: latestTask && ['completed', 'failed'].includes(latestTask.status) ? 1 : 0,
1406
+ taskResultsFailed: latestTask?.status === 'failed' ? 1 : 0
1407
+ })
1408
+ };
1409
+
1410
+ if (hasThumbnail) {
1411
+ const frame = makeSyntheticFrame(device, `synthetic-thumb-${ordinal}`, 'thumbnail', {
1412
+ width: 360,
1413
+ height: 220
1414
+ });
1415
+ delete frame.mode;
1416
+ delete frame.fps;
1417
+ frame.receivedAt = seenAt;
1418
+ frame.capturedAt = seenAt;
1419
+ device.latestThumbnail = frame;
1420
+ rememberRecentFramePayload(device, 'thumbnail', frame);
1421
+ device.counters.thumbnailFramesReceived = 1;
1422
+ }
1423
+
1424
+ if (isLive) {
1425
+ device.activeLiveStream = {
1426
+ streamId: `synthetic-live-${ordinal}`,
1427
+ commandId: `synthetic-live-command-${ordinal}`,
1428
+ active: true,
1429
+ mode: 'remote-fast',
1430
+ fps: 20,
1431
+ startedAt: seenAt,
1432
+ stoppedAt: '',
1433
+ stopReason: '',
1434
+ lastFrameAt: '',
1435
+ lastFrameSeq: 0,
1436
+ framesReceived: 0
1437
+ };
1438
+ applySyntheticLiveFrame(device, device.activeLiveStream.streamId, {
1439
+ commandId: device.activeLiveStream.commandId,
1440
+ fps: 20
1441
+ });
1442
+ live += 1;
1443
+ }
1444
+
1445
+ if (isConnected) {
1446
+ connected += 1;
1447
+ }
1448
+ if (hasAiAssist) {
1449
+ aiAssist += 1;
1450
+ }
1451
+ devices.set(deviceId, device);
1452
+ }
1453
+
1454
+ emitRemoteEvent('RemoteSyntheticFleetSeeded', null, {
1455
+ count,
1456
+ connected,
1457
+ aiAssist,
1458
+ live
1459
+ });
1460
+ return {
1461
+ ok: true,
1462
+ synthetic: true,
1463
+ seeded: count,
1464
+ connected,
1465
+ aiAssist,
1466
+ live,
1467
+ total: devices.size,
1468
+ max: MAX_SYNTHETIC_DEVICES
1469
+ };
1470
+ }
1471
+
1472
+ function attachDevice(socket, hello) {
1473
+ const now = new Date().toISOString();
1474
+ const sessionId = crypto.randomUUID();
1475
+ const deviceId = normalizeDeviceId(hello.deviceId);
1476
+
1477
+ closeExistingDeviceSocket(deviceId, sessionId);
1478
+
1479
+ const device = {
1480
+ socket,
1481
+ deviceId,
1482
+ sessionId,
1483
+ deviceName: safeString(hello.deviceName || hello.hostname || deviceId, 120),
1484
+ hostname: safeString(hello.hostname, 120),
1485
+ platform: safeString(hello.platform, 80),
1486
+ arch: safeString(hello.arch, 40),
1487
+ pid: Number.isFinite(Number(hello.pid)) ? Number(hello.pid) : 0,
1488
+ agentVersion: safeString(hello.agentVersion, 40),
1489
+ capabilities: typeof hello.capabilities === 'object' && hello.capabilities
1490
+ ? { ...hello.capabilities }
1491
+ : {},
1492
+ connected: true,
1493
+ connectedAt: now,
1494
+ disconnectedAt: '',
1495
+ lastSeenAt: now,
1496
+ lastStatusAt: '',
1497
+ lastDisconnectReason: '',
1498
+ remoteAddress: socket.remoteAddress || '',
1499
+ remotePort: socket.remotePort || 0,
1500
+ status: {},
1501
+ latestThumbnail: null,
1502
+ latestLiveFrame: null,
1503
+ recentFramePayloads: {
1504
+ thumbnail: [],
1505
+ live: []
1506
+ },
1507
+ activeLiveStream: null,
1508
+ latestTask: null,
1509
+ recentTasks: [],
1510
+ pendingTaskCommands: new Map(),
1511
+ pendingTaskTimers: new Map(),
1512
+ synthetic: false,
1513
+ counters: createDeviceCounters({ messagesReceived: 1 })
1514
+ };
1515
+
1516
+ devices.set(deviceId, device);
1517
+ sockets.set(socket, deviceId);
1518
+ writeJsonLine(socket, {
1519
+ type: 'welcome',
1520
+ protocolVersion: REMOTE_PROTOCOL_VERSION,
1521
+ sessionId,
1522
+ deviceId,
1523
+ heartbeatMs,
1524
+ serverTime: now
1525
+ });
1526
+
1527
+ logEvent('remote', `device connected ${device.deviceName} (${deviceId})`, 'success');
1528
+ emitRemoteEvent('RemoteDeviceConnected', device);
1529
+ return device;
1530
+ }
1531
+
1532
+ function detachSocket(socket, reason = 'socket-closed') {
1533
+ const deviceId = sockets.get(socket);
1534
+ sockets.delete(socket);
1535
+ if (!deviceId) {
1536
+ return;
1537
+ }
1538
+
1539
+ const device = devices.get(deviceId);
1540
+ if (!device || device.socket !== socket) {
1541
+ return;
1542
+ }
1543
+
1544
+ device.connected = false;
1545
+ device.socket = null;
1546
+ device.disconnectedAt = new Date().toISOString();
1547
+ device.lastDisconnectReason = reason;
1548
+ if (device.activeLiveStream) {
1549
+ device.activeLiveStream.active = false;
1550
+ device.activeLiveStream.stoppedAt = device.disconnectedAt;
1551
+ device.activeLiveStream.stopReason = reason;
1552
+ }
1553
+ failAllPendingTasks(device, 'device-disconnected');
1554
+ logWarn('remote', `device disconnected ${device.deviceName} (${deviceId}): ${reason}`);
1555
+ emitRemoteEvent('RemoteDeviceDisconnected', device, { reason });
1556
+ }
1557
+
1558
+ function rememberDeviceTask(device, task) {
1559
+ if (!device || !task) {
1560
+ return null;
1561
+ }
1562
+
1563
+ const existingIndex = device.recentTasks.findIndex(item =>
1564
+ item.taskId === task.taskId || item.commandId === task.commandId);
1565
+ if (existingIndex >= 0) {
1566
+ device.recentTasks.splice(existingIndex, 1);
1567
+ }
1568
+
1569
+ device.recentTasks.unshift(task);
1570
+ if (device.recentTasks.length > RECENT_TASK_LIMIT) {
1571
+ device.recentTasks.length = RECENT_TASK_LIMIT;
1572
+ }
1573
+
1574
+ device.latestTask = task;
1575
+ if (task.commandId) {
1576
+ device.pendingTaskCommands.set(task.commandId, task);
1577
+ }
1578
+
1579
+ return task;
1580
+ }
1581
+
1582
+ function trimTaskBatches() {
1583
+ const ordered = [...taskBatches.values()]
1584
+ .sort((left, right) => String(right.requestedAt || '').localeCompare(String(left.requestedAt || '')));
1585
+ for (const batch of ordered.slice(RECENT_TASK_BATCH_LIMIT)) {
1586
+ taskBatches.delete(batch.batchId);
1587
+ }
1588
+ }
1589
+
1590
+ function beginTaskBatch(deviceIds, options = {}) {
1591
+ const targets = [...new Set((deviceIds || [])
1592
+ .map(deviceId => safeString(deviceId, 128))
1593
+ .filter(Boolean))]
1594
+ .slice(0, 500);
1595
+ const now = new Date().toISOString();
1596
+ const instruction = safeText(options.instruction, MAX_AGENT_TASK_CHARS);
1597
+ const batchId = safeString(options.batchId, 128) || crypto.randomUUID();
1598
+ const title = safeString(options.title, 120)
1599
+ || safeString(instruction.split(/\r?\n/)[0], 120)
1600
+ || 'Remote task batch';
1601
+ const batch = {
1602
+ batchId,
1603
+ title,
1604
+ instructionPreview: safeText(instruction, 320),
1605
+ approvalLevel: normalizeApprovalLevel(options.approvalLevel),
1606
+ status: targets.length > 0 ? 'running' : 'failed',
1607
+ requestedAt: now,
1608
+ updatedAt: now,
1609
+ total: targets.length,
1610
+ queued: 0,
1611
+ pending: 0,
1612
+ completed: 0,
1613
+ failed: targets.length > 0 ? 0 : 1,
1614
+ timedOut: 0,
1615
+ results: targets.map(deviceId => ({
1616
+ deviceId,
1617
+ ok: false,
1618
+ status: 'targeted',
1619
+ error: '',
1620
+ commandId: '',
1621
+ taskId: '',
1622
+ approvalLevel: normalizeApprovalLevel(options.approvalLevel),
1623
+ updatedAt: now
1624
+ }))
1625
+ };
1626
+
1627
+ taskBatches.set(batchId, batch);
1628
+ trimTaskBatches();
1629
+ return batch;
1630
+ }
1631
+
1632
+ function getTaskBatchItem(batch, deviceId) {
1633
+ if (!batch) {
1634
+ return null;
1635
+ }
1636
+
1637
+ const id = safeString(deviceId, 128);
1638
+ if (!id) {
1639
+ return null;
1640
+ }
1641
+
1642
+ let item = batch.results.find(result => result.deviceId === id);
1643
+ if (!item) {
1644
+ item = {
1645
+ deviceId: id,
1646
+ ok: false,
1647
+ status: 'targeted',
1648
+ error: '',
1649
+ commandId: '',
1650
+ taskId: '',
1651
+ approvalLevel: batch.approvalLevel,
1652
+ updatedAt: new Date().toISOString()
1653
+ };
1654
+ batch.results.push(item);
1655
+ batch.total = batch.results.length;
1656
+ }
1657
+
1658
+ return item;
1659
+ }
1660
+
1661
+ function recalculateTaskBatch(batch) {
1662
+ if (!batch) {
1663
+ return null;
1664
+ }
1665
+
1666
+ const results = Array.isArray(batch.results) ? batch.results : [];
1667
+ batch.total = results.length;
1668
+ batch.queued = results.filter(item => item.ok === true).length;
1669
+ batch.completed = results.filter(item => item.status === 'completed').length;
1670
+ batch.failed = results.filter(item => item.status === 'failed' || (item.ok === false && !!item.error)).length;
1671
+ batch.timedOut = results.filter(item => item.error === 'task-timeout').length;
1672
+ batch.pending = results.filter(item =>
1673
+ item.ok === true
1674
+ && item.status !== 'completed'
1675
+ && item.status !== 'failed').length;
1676
+ batch.updatedAt = new Date().toISOString();
1677
+
1678
+ if (batch.total === 0) {
1679
+ batch.status = 'failed';
1680
+ } else if (batch.completed + batch.failed >= batch.total) {
1681
+ batch.status = batch.failed > 0 ? 'completed-with-failures' : 'completed';
1682
+ } else if (batch.queued > 0) {
1683
+ batch.status = 'running';
1684
+ } else {
1685
+ batch.status = 'failed';
1686
+ }
1687
+
1688
+ return batch;
1689
+ }
1690
+
1691
+ function syncTaskBatchFromTask(device, task) {
1692
+ const batchId = safeString(task?.batchId, 128);
1693
+ if (!batchId) {
1694
+ return null;
1695
+ }
1696
+
1697
+ const batch = taskBatches.get(batchId);
1698
+ if (!batch) {
1699
+ return null;
1700
+ }
1701
+
1702
+ const item = getTaskBatchItem(batch, device?.deviceId);
1703
+ if (!item) {
1704
+ return null;
1705
+ }
1706
+
1707
+ item.ok = true;
1708
+ item.status = safeString(task.status, 40) || 'queued';
1709
+ item.error = safeString(task.error, 500);
1710
+ item.commandId = safeString(task.commandId, 128);
1711
+ item.taskId = safeString(task.taskId, 128);
1712
+ item.approvalLevel = normalizeApprovalLevel(task.approvalLevel || batch.approvalLevel);
1713
+ item.updatedAt = task.updatedAt || task.completedAt || task.sentAt || new Date().toISOString();
1714
+ return recalculateTaskBatch(batch);
1715
+ }
1716
+
1717
+ function syncTaskBatchDispatchFailure(batchId, deviceId, error, extra = {}) {
1718
+ const batch = taskBatches.get(safeString(batchId, 128));
1719
+ if (!batch) {
1720
+ return null;
1721
+ }
1722
+
1723
+ const item = getTaskBatchItem(batch, deviceId);
1724
+ if (!item) {
1725
+ return null;
1726
+ }
1727
+
1728
+ item.ok = false;
1729
+ item.status = 'failed';
1730
+ item.error = safeString(error, 500) || 'task-dispatch-failed';
1731
+ item.commandId = safeString(extra.commandId, 128);
1732
+ item.taskId = safeString(extra.taskId, 128);
1733
+ item.approvalLevel = normalizeApprovalLevel(extra.approvalLevel || batch.approvalLevel);
1734
+ item.updatedAt = new Date().toISOString();
1735
+ return recalculateTaskBatch(batch);
1736
+ }
1737
+
1738
+ function clearTaskTimeout(device, commandId) {
1739
+ const key = safeString(commandId, 128);
1740
+ if (!device?.pendingTaskTimers || !key) {
1741
+ return;
1742
+ }
1743
+
1744
+ const timer = device.pendingTaskTimers.get(key);
1745
+ if (timer) {
1746
+ clearTimeout(timer);
1747
+ }
1748
+ device.pendingTaskTimers.delete(key);
1749
+ }
1750
+
1751
+ function failPendingTask(device, commandId, error = 'task-timeout') {
1752
+ const key = safeString(commandId, 128);
1753
+ if (!device?.pendingTaskCommands || !key) {
1754
+ return null;
1755
+ }
1756
+
1757
+ const task = device.pendingTaskCommands.get(key);
1758
+ if (!task) {
1759
+ clearTaskTimeout(device, key);
1760
+ return null;
1761
+ }
1762
+
1763
+ const now = new Date().toISOString();
1764
+ clearTaskTimeout(device, key);
1765
+ task.status = 'failed';
1766
+ task.updatedAt = now;
1767
+ task.completedAt = now;
1768
+ task.error = safeString(error, 500) || 'task-timeout';
1769
+ task.resultSummary = task.error;
1770
+ task.resultKind = task.resultKind || 'agent-task';
1771
+ device.latestTask = task;
1772
+ device.pendingTaskCommands.delete(key);
1773
+ device.counters.taskResultsReceived += 1;
1774
+ device.counters.taskResultsFailed += 1;
1775
+ if (task.error === 'task-timeout') {
1776
+ device.counters.taskResultsTimedOut += 1;
1777
+ }
1778
+ syncTaskBatchFromTask(device, task);
1779
+ emitRemoteEvent('RemoteTaskResult', device, {
1780
+ commandId: key,
1781
+ taskId: task.taskId,
1782
+ status: task.status,
1783
+ error: task.error
1784
+ });
1785
+ return task;
1786
+ }
1787
+
1788
+ function failAllPendingTasks(device, error = 'device-disconnected') {
1789
+ if (!device?.pendingTaskCommands) {
1790
+ return 0;
1791
+ }
1792
+
1793
+ const commandIds = [...device.pendingTaskCommands.keys()];
1794
+ let failed = 0;
1795
+ for (const commandId of commandIds) {
1796
+ if (failPendingTask(device, commandId, error)) {
1797
+ failed += 1;
1798
+ }
1799
+ }
1800
+ return failed;
1801
+ }
1802
+
1803
+ function scheduleTaskTimeout(device, task) {
1804
+ const commandId = safeString(task?.commandId, 128);
1805
+ if (!device?.pendingTaskTimers || !commandId) {
1806
+ return;
1807
+ }
1808
+
1809
+ clearTaskTimeout(device, commandId);
1810
+ const timer = setTimeout(() => {
1811
+ failPendingTask(device, commandId, 'task-timeout');
1812
+ }, taskTimeoutMs);
1813
+ timer.unref?.();
1814
+ device.pendingTaskTimers.set(commandId, timer);
1815
+ }
1816
+
1817
+ function summarizeTaskResult(result) {
1818
+ if (result && typeof result === 'object') {
1819
+ return safeText(
1820
+ result.summary
1821
+ || result.message
1822
+ || result.output
1823
+ || result.result
1824
+ || JSON.stringify(result),
1825
+ MAX_AGENT_TASK_RESULT_CHARS);
1826
+ }
1827
+
1828
+ return safeText(result ?? '', MAX_AGENT_TASK_RESULT_CHARS);
1829
+ }
1830
+
1831
+ function applyTaskResult(device, commandId, result, error) {
1832
+ if (!device || !commandId) {
1833
+ return null;
1834
+ }
1835
+
1836
+ const resultTaskId = result && typeof result === 'object'
1837
+ ? safeString(result.taskId, 128)
1838
+ : '';
1839
+ const task = resultTaskId
1840
+ ? device.recentTasks.find(item => item.taskId === resultTaskId)
1841
+ : device.pendingTaskCommands.get(commandId);
1842
+ if (!task) {
1843
+ return null;
1844
+ }
1845
+
1846
+ if (device.pendingTaskCommands.get(commandId) !== task) {
1847
+ return null;
1848
+ }
1849
+
1850
+ const now = device.lastSeenAt || new Date().toISOString();
1851
+ const status = error
1852
+ ? 'failed'
1853
+ : safeString(result?.status, 40) || 'completed';
1854
+ clearTaskTimeout(device, commandId);
1855
+ task.status = status;
1856
+ task.updatedAt = now;
1857
+ task.completedAt = safeString(result?.completedAt, 80) || now;
1858
+ task.error = error;
1859
+ task.resultSummary = error || summarizeTaskResult(result);
1860
+ task.resultKind = safeString(result?.kind || result?.mode || 'agent-task', 80);
1861
+ task.resultModel = safeString(result?.model || result?.aiModel || '', 120);
1862
+ task.resultResponseId = safeString(result?.responseId || result?.id || '', 160);
1863
+ device.latestTask = task;
1864
+ device.pendingTaskCommands.delete(commandId);
1865
+ device.counters.taskResultsReceived += 1;
1866
+ if (error) {
1867
+ device.counters.taskResultsFailed += 1;
1868
+ }
1869
+ syncTaskBatchFromTask(device, task);
1870
+
1871
+ return task;
1872
+ }
1873
+
1874
+ function normalizeFrameByteLength(framePayload, frameData = '') {
1875
+ if (Buffer.isBuffer(framePayload)) {
1876
+ return framePayload.length;
1877
+ }
1878
+
1879
+ return Math.floor(String(frameData || '').length * 3 / 4);
1880
+ }
1881
+
1882
+ function buildFrameDataUrl(framePayload, frameData, mimeType) {
1883
+ if (Buffer.isBuffer(framePayload)) {
1884
+ return `data:${mimeType};base64,${framePayload.toString('base64')}`;
1885
+ }
1886
+
1887
+ return String(frameData || '').startsWith('data:')
1888
+ ? String(frameData || '')
1889
+ : `data:${mimeType};base64,${frameData}`;
1890
+ }
1891
+
1892
+ function buildFramePayloadBuffer(framePayload, frameData) {
1893
+ if (Buffer.isBuffer(framePayload)) {
1894
+ return Buffer.from(framePayload);
1895
+ }
1896
+
1897
+ const raw = String(frameData || '');
1898
+ const commaIndex = raw.indexOf(',');
1899
+ const base64 = raw.startsWith('data:') && commaIndex >= 0
1900
+ ? raw.slice(commaIndex + 1)
1901
+ : raw;
1902
+ return Buffer.from(base64, 'base64');
1903
+ }
1904
+
1905
+ function buildFrameContentHash(message, payload) {
1906
+ const provided = safeString(message.contentHash || message.frameHash || message.hash, 96)
1907
+ .toLowerCase()
1908
+ .replace(/[^a-z0-9:_-]/g, '')
1909
+ .slice(0, 64);
1910
+ if (provided) {
1911
+ return provided;
1912
+ }
1913
+
1914
+ if (!Buffer.isBuffer(payload) || payload.length === 0) {
1915
+ return '';
1916
+ }
1917
+
1918
+ return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 16);
1919
+ }
1920
+
1921
+ function readFrameCaptureMs(message) {
1922
+ const value = Number(message.captureMs);
1923
+ return Number.isFinite(value) && value >= 0
1924
+ ? Math.round(value)
1925
+ : 0;
1926
+ }
1927
+
1928
+ function readFrameDroppedByAgent(message) {
1929
+ const value = Number(message.droppedByAgent ?? message.agentDrops ?? message.drops);
1930
+ return Number.isFinite(value) && value >= 0
1931
+ ? Math.round(value)
1932
+ : 0;
1933
+ }
1934
+
1935
+ function readFrameDirtyRectCount(message) {
1936
+ if (Array.isArray(message.dirtyRects)) {
1937
+ return message.dirtyRects.length;
1938
+ }
1939
+
1940
+ const value = Number(message.dirtyRectCount ?? message.dirtyRegionCount ?? message.changedRegionCount);
1941
+ return Number.isFinite(value) && value >= 0
1942
+ ? Math.round(value)
1943
+ : 0;
1944
+ }
1945
+
1946
+ function readFrameDirtyPixelRatio(message) {
1947
+ const value = Number(message.dirtyPixelRatio ?? message.dirtyRatio ?? message.dirtyAreaRatio ?? message.changedPixelRatio);
1948
+ if (!Number.isFinite(value) || value < 0) {
1949
+ return 0;
1950
+ }
1951
+
1952
+ if (value <= 1) {
1953
+ return value;
1954
+ }
1955
+
1956
+ if (value <= 100) {
1957
+ return value / 100;
1958
+ }
1959
+
1960
+ return 1;
1961
+ }
1962
+
1963
+ function readFrameDeltaEncoded(message) {
1964
+ const value = message.deltaEncoded ?? message.isDeltaEncoded ?? message.deltaFrame;
1965
+ if (typeof value === 'boolean') {
1966
+ return value;
1967
+ }
1968
+
1969
+ if (typeof value === 'number') {
1970
+ return value > 0;
1971
+ }
1972
+
1973
+ return /^(1|true|yes|on|delta)$/i.test(String(value || '').trim());
1974
+ }
1975
+
1976
+ function computeSameContentStreak(previousFrame, contentHash) {
1977
+ if (!contentHash || !previousFrame?.contentHash || previousFrame.contentHash !== contentHash) {
1978
+ return 0;
1979
+ }
1980
+
1981
+ return clampNumber(Number(previousFrame.sameContentStreak || 0) + 1, 0, 1000000, 1);
1982
+ }
1983
+
1984
+ function applyThumbnailFrame(device, message, framePayload, transport = 'json-base64') {
1985
+ const frameData = Buffer.isBuffer(framePayload)
1986
+ ? ''
1987
+ : safeString(framePayload, MAX_THUMBNAIL_BASE64_CHARS + 1);
1988
+ const frameSeq = Number(message.frameSeq);
1989
+ const byteLength = normalizeFrameByteLength(framePayload, frameData);
1990
+ if ((!Buffer.isBuffer(framePayload) && !frameData)
1991
+ || byteLength > MAX_THUMBNAIL_BINARY_BYTES
1992
+ || !Number.isFinite(frameSeq)) {
1993
+ device.counters.thumbnailFramesDropped += 1;
1994
+ emitRemoteEvent('RemoteFrameDropped', device, {
1995
+ reason: 'invalid-thumbnail-frame',
1996
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
1997
+ transport
1998
+ });
1999
+ return false;
2000
+ }
2001
+
2002
+ const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
2003
+ const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
2004
+ const payload = buildFramePayloadBuffer(framePayload, frameData);
2005
+ const contentHash = buildFrameContentHash(message, payload);
2006
+ const sameContentStreak = computeSameContentStreak(device.latestThumbnail, contentHash);
2007
+ const droppedByAgent = readFrameDroppedByAgent(message);
2008
+ const dirtyRectCount = readFrameDirtyRectCount(message);
2009
+ const dirtyPixelRatio = readFrameDirtyPixelRatio(message);
2010
+ const codec = safeString(message.codec || message.encoder || message.encoding, 80);
2011
+ const deltaEncoded = readFrameDeltaEncoded(message);
2012
+ device.latestThumbnail = {
2013
+ streamId: safeString(message.streamId, 128) || 'thumbnail',
2014
+ frameSeq,
2015
+ commandId: safeString(message.commandId, 128),
2016
+ width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
2017
+ height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
2018
+ mimeType,
2019
+ format: mimeType,
2020
+ capturedAt,
2021
+ receivedAt: device.lastSeenAt,
2022
+ byteLength,
2023
+ transport,
2024
+ contentHash,
2025
+ captureMs: readFrameCaptureMs(message),
2026
+ sameContentStreak,
2027
+ droppedByAgent,
2028
+ dirtyRectCount,
2029
+ dirtyPixelRatio,
2030
+ codec,
2031
+ deltaEncoded,
2032
+ payload,
2033
+ accessToken: createFrameAccessToken()
2034
+ };
2035
+ rememberRecentFramePayload(device, 'thumbnail', device.latestThumbnail);
2036
+ device.counters.thumbnailFramesReceived += 1;
2037
+ emitRemoteEvent('RemoteFrameReceived', device, {
2038
+ streamId: device.latestThumbnail.streamId,
2039
+ frameSeq,
2040
+ width: device.latestThumbnail.width,
2041
+ height: device.latestThumbnail.height,
2042
+ transport,
2043
+ contentHash,
2044
+ sameContentStreak,
2045
+ droppedByAgent,
2046
+ dirtyRectCount,
2047
+ dirtyPixelRatio,
2048
+ codec,
2049
+ deltaEncoded
2050
+ });
2051
+ return true;
2052
+ }
2053
+
2054
+ function applyLiveFrame(device, message, framePayload, transport = 'json-base64') {
2055
+ const frameData = Buffer.isBuffer(framePayload)
2056
+ ? ''
2057
+ : safeString(framePayload, MAX_STREAM_BASE64_CHARS + 1);
2058
+ const frameSeq = Number(message.frameSeq);
2059
+ const streamId = safeString(message.streamId, 128) || 'live';
2060
+ if (!device.activeLiveStream?.active || device.activeLiveStream.streamId !== streamId) {
2061
+ device.counters.liveFramesDropped += 1;
2062
+ emitRemoteEvent('RemoteFrameDropped', device, {
2063
+ reason: 'stale-live-stream-frame',
2064
+ streamId,
2065
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
2066
+ transport
2067
+ });
2068
+ return false;
2069
+ }
2070
+
2071
+ const byteLength = normalizeFrameByteLength(framePayload, frameData);
2072
+ if ((!Buffer.isBuffer(framePayload) && !frameData)
2073
+ || byteLength > MAX_STREAM_BINARY_BYTES
2074
+ || !Number.isFinite(frameSeq)) {
2075
+ device.counters.liveFramesDropped += 1;
2076
+ emitRemoteEvent('RemoteFrameDropped', device, {
2077
+ reason: 'invalid-live-frame',
2078
+ streamId,
2079
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
2080
+ transport
2081
+ });
2082
+ return false;
2083
+ }
2084
+
2085
+ const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
2086
+ const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
2087
+ const payload = buildFramePayloadBuffer(framePayload, frameData);
2088
+ const contentHash = buildFrameContentHash(message, payload);
2089
+ const sameContentStreak = computeSameContentStreak(device.latestLiveFrame, contentHash);
2090
+ const droppedByAgent = readFrameDroppedByAgent(message);
2091
+ const dirtyRectCount = readFrameDirtyRectCount(message);
2092
+ const dirtyPixelRatio = readFrameDirtyPixelRatio(message);
2093
+ const codec = safeString(message.codec || message.encoder || message.encoding, 80);
2094
+ const deltaEncoded = readFrameDeltaEncoded(message);
2095
+ device.latestLiveFrame = {
2096
+ streamId,
2097
+ frameSeq,
2098
+ commandId: safeString(message.commandId, 128),
2099
+ width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
2100
+ height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
2101
+ mimeType,
2102
+ format: mimeType,
2103
+ mode: safeString(message.mode || device.activeLiveStream.mode || 'remote-fast', 80),
2104
+ fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : device.activeLiveStream.fps,
2105
+ capturedAt,
2106
+ receivedAt: device.lastSeenAt,
2107
+ byteLength,
2108
+ transport,
2109
+ contentHash,
2110
+ captureMs: readFrameCaptureMs(message),
2111
+ sameContentStreak,
2112
+ droppedByAgent,
2113
+ dirtyRectCount,
2114
+ dirtyPixelRatio,
2115
+ codec,
2116
+ deltaEncoded,
2117
+ payload,
2118
+ accessToken: createFrameAccessToken()
2119
+ };
2120
+ rememberRecentFramePayload(device, 'live', device.latestLiveFrame);
2121
+ emitFrame({
2122
+ kind: 'live',
2123
+ deviceId: device.deviceId,
2124
+ frame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', { includeDataUrl: false }),
2125
+ payload: device.latestLiveFrame.payload,
2126
+ mimeType: device.latestLiveFrame.mimeType,
2127
+ byteLength: device.latestLiveFrame.byteLength
2128
+ });
2129
+ device.activeLiveStream.lastFrameAt = device.lastSeenAt;
2130
+ device.activeLiveStream.lastFrameSeq = frameSeq;
2131
+ device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
2132
+ device.counters.liveFramesReceived += 1;
2133
+ emitRemoteEvent('RemoteFrameReceived', device, {
2134
+ streamId,
2135
+ frameSeq,
2136
+ width: device.latestLiveFrame.width,
2137
+ height: device.latestLiveFrame.height,
2138
+ mode: device.latestLiveFrame.mode,
2139
+ transport,
2140
+ contentHash,
2141
+ sameContentStreak,
2142
+ droppedByAgent,
2143
+ dirtyRectCount,
2144
+ dirtyPixelRatio,
2145
+ codec,
2146
+ deltaEncoded
2147
+ });
2148
+ return true;
2149
+ }
2150
+
2151
+ function handleAgentBinaryFrame(socket, state, header, framePayload) {
2152
+ if (!state.authenticated || !state.device) {
2153
+ writeJsonLine(socket, { type: 'error', error: 'hello-required' });
2154
+ socket.destroy();
2155
+ return;
2156
+ }
2157
+
2158
+ const device = state.device;
2159
+ device.counters.messagesReceived += 1;
2160
+ device.lastSeenAt = new Date().toISOString();
2161
+
2162
+ const frameKind = safeString(header.frameKind || header.kind || header.frameType || '', 40).toLowerCase();
2163
+ if (frameKind === 'thumbnail' || header.type === 'thumbnail.binary') {
2164
+ applyThumbnailFrame(device, header, framePayload, 'binary');
2165
+ return;
2166
+ }
2167
+
2168
+ if (frameKind === 'stream' || frameKind === 'live' || header.type === 'stream.binary') {
2169
+ applyLiveFrame(device, header, framePayload, 'binary');
2170
+ return;
2171
+ }
2172
+
2173
+ emitRemoteEvent('RemoteAgentMessageIgnored', device, {
2174
+ messageType: safeString(header.type, 80),
2175
+ frameKind
2176
+ });
2177
+ }
2178
+
2179
+ function handleAgentMessage(socket, state, message) {
2180
+ if (!message || typeof message !== 'object') {
2181
+ return;
2182
+ }
2183
+
2184
+ if (!state.authenticated) {
2185
+ if (message.type !== 'hello') {
2186
+ writeJsonLine(socket, { type: 'error', error: 'hello-required' });
2187
+ socket.destroy();
2188
+ return;
2189
+ }
2190
+
2191
+ if (!timingSafeStringEqual(message.pairToken, pairToken)) {
2192
+ writeJsonLine(socket, { type: 'error', error: 'invalid-pair-token' });
2193
+ socket.destroy();
2194
+ return;
2195
+ }
2196
+
2197
+ state.authenticated = true;
2198
+ state.device = attachDevice(socket, message);
2199
+ return;
2200
+ }
2201
+
2202
+ const device = state.device;
2203
+ if (!device) {
2204
+ socket.destroy();
2205
+ return;
2206
+ }
2207
+
2208
+ device.counters.messagesReceived += 1;
2209
+ device.lastSeenAt = new Date().toISOString();
2210
+
2211
+ switch (message.type) {
2212
+ case 'heartbeat':
2213
+ case 'status':
2214
+ device.status = typeof message.status === 'object' && message.status
2215
+ ? { ...message.status }
2216
+ : {};
2217
+ device.lastStatusAt = device.lastSeenAt;
2218
+ device.counters.statusReceived += 1;
2219
+ emitRemoteEvent('RemoteDeviceStatus', device);
2220
+ break;
2221
+ case 'command.result':
2222
+ device.counters.commandResultsReceived += 1;
2223
+ {
2224
+ const commandId = safeString(message.commandId, 128);
2225
+ const error = safeString(message.error, 500);
2226
+ const task = applyTaskResult(device, commandId, message.result ?? null, error);
2227
+ if (task) {
2228
+ emitRemoteEvent('RemoteTaskResult', device, {
2229
+ commandId,
2230
+ taskId: task.taskId,
2231
+ status: task.status,
2232
+ error: task.error
2233
+ });
2234
+ }
2235
+ }
2236
+ emitRemoteEvent('RemoteCommandResult', device, {
2237
+ commandId: safeString(message.commandId, 128),
2238
+ result: message.result ?? null,
2239
+ error: safeString(message.error, 500)
2240
+ });
2241
+ break;
2242
+ case 'thumbnail.frame': {
2243
+ applyThumbnailFrame(device, message, message.data, 'json-base64');
2244
+ break;
2245
+ }
2246
+ case 'stream.frame': {
2247
+ applyLiveFrame(device, message, message.data, 'json-base64');
2248
+ break;
2249
+ }
2250
+ default:
2251
+ emitRemoteEvent('RemoteAgentMessageIgnored', device, {
2252
+ messageType: safeString(message.type, 80)
2253
+ });
2254
+ break;
2255
+ }
2256
+ }
2257
+
2258
+ function handleWebSocketAgentSocket(socket) {
2259
+ allSockets.add(socket);
2260
+ socket.setNoDelay(true);
2261
+ socket.setKeepAlive(true, heartbeatMs);
2262
+
2263
+ const state = {
2264
+ authenticated: false,
2265
+ device: null
2266
+ };
2267
+
2268
+ const helloTimer = setTimeout(() => {
2269
+ if (!state.authenticated) {
2270
+ writeJsonLine(socket, { type: 'error', error: 'hello-timeout' });
2271
+ socket.destroy();
2272
+ }
2273
+ }, 10000);
2274
+
2275
+ socket.onTextMessage = text => {
2276
+ try {
2277
+ handleAgentMessage(socket, state, parseJsonLine(String(text || '')));
2278
+ } catch (err) {
2279
+ writeJsonLine(socket, { type: 'error', error: 'invalid-json' });
2280
+ logWarn('remote', `invalid websocket agent message: ${err?.message || err}`);
2281
+ }
2282
+ };
2283
+
2284
+ socket.onBinaryMessage = payload => {
2285
+ try {
2286
+ const packet = parseRemoteHubWebSocketBinaryFrame(payload);
2287
+ if (!packet?.header || !Buffer.isBuffer(packet.payload)) {
2288
+ writeJsonLine(socket, { type: 'error', error: 'invalid-websocket-binary-frame' });
2289
+ return;
2290
+ }
2291
+
2292
+ const frameKind = safeString(packet.header.frameKind || packet.header.kind || packet.header.frameType, 40).toLowerCase();
2293
+ const maxBytes = frameKind === 'thumbnail'
2294
+ ? MAX_THUMBNAIL_BINARY_BYTES
2295
+ : MAX_STREAM_BINARY_BYTES;
2296
+ const byteLength = Number(packet.header.byteLength ?? packet.header.payloadBytes ?? packet.payload.length);
2297
+ if (!Number.isFinite(byteLength)
2298
+ || byteLength < 1
2299
+ || byteLength > maxBytes
2300
+ || byteLength !== packet.payload.length) {
2301
+ writeJsonLine(socket, { type: 'error', error: 'invalid-binary-frame' });
2302
+ logWarn('remote', 'invalid websocket binary frame from agent.');
2303
+ return;
2304
+ }
2305
+
2306
+ handleAgentBinaryFrame(socket, state, packet.header, packet.payload);
2307
+ } catch (err) {
2308
+ writeJsonLine(socket, { type: 'error', error: 'invalid-websocket-binary-frame' });
2309
+ logWarn('remote', `invalid websocket binary frame: ${err?.message || err}`);
2310
+ }
2311
+ };
2312
+
2313
+ const detach = reason => {
2314
+ allSockets.delete(socket);
2315
+ clearTimeout(helloTimer);
2316
+ detachSocket(socket, reason);
2317
+ };
2318
+
2319
+ socket.onCloseMessage = reason => detach(reason || 'websocket-closed');
2320
+ socket.socket.on('close', () => detach('websocket-closed'));
2321
+ socket.socket.on('error', err => detach(err?.message || 'websocket-error'));
2322
+ }
2323
+
2324
+ function handleWebSocketUpgradeSocket(socket, firstChunk) {
2325
+ let upgradeBuffer = Buffer.isBuffer(firstChunk) ? firstChunk : Buffer.from(firstChunk);
2326
+ let onUpgradeData = null;
2327
+ const fail = (status, message) => {
2328
+ try {
2329
+ socket.write(`HTTP/1.1 ${status} ${message}\r\nConnection: close\r\n\r\n`);
2330
+ } catch {
2331
+ // Ignore write failures while rejecting the upgrade.
2332
+ }
2333
+ socket.destroy();
2334
+ };
2335
+
2336
+ const completeUpgrade = () => {
2337
+ const request = parseRemoteHubWebSocketUpgrade(upgradeBuffer);
2338
+ if (!request) {
2339
+ return false;
2340
+ }
2341
+
2342
+ const upgrade = String(request.headers.upgrade || '').toLowerCase();
2343
+ const connection = String(request.headers.connection || '').toLowerCase();
2344
+ const key = safeString(request.headers['sec-websocket-key'], 256);
2345
+ const pathName = safeString(String(request.path || '').split('?')[0], 128) || '/';
2346
+ const allowedPath = pathName === '/'
2347
+ || pathName === '/remote-agent'
2348
+ || pathName === '/api/remote/agent/ws';
2349
+ if (request.method !== 'GET'
2350
+ || upgrade !== 'websocket'
2351
+ || !connection.includes('upgrade')
2352
+ || !key
2353
+ || !allowedPath) {
2354
+ fail(400, 'Bad Request');
2355
+ return true;
2356
+ }
2357
+
2358
+ const acceptKey = buildRemoteHubWebSocketAcceptKey(key);
2359
+ socket.write([
2360
+ 'HTTP/1.1 101 Switching Protocols',
2361
+ 'Upgrade: websocket',
2362
+ 'Connection: Upgrade',
2363
+ `Sec-WebSocket-Accept: ${acceptKey}`,
2364
+ '\r\n'
2365
+ ].join('\r\n'));
2366
+
2367
+ if (onUpgradeData) {
2368
+ socket.removeListener('data', onUpgradeData);
2369
+ }
2370
+ const wsSocket = new RemoteHubWebSocketAgentSocket(socket);
2371
+ const onFrameData = chunk => wsSocket.handleData(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2372
+ socket.on('data', onFrameData);
2373
+ socket.once('close', () => {
2374
+ socket.removeListener('data', onFrameData);
2375
+ });
2376
+ handleWebSocketAgentSocket(wsSocket);
2377
+ if (request.head.length > 0) {
2378
+ wsSocket.handleData(request.head);
2379
+ }
2380
+ return true;
2381
+ };
2382
+
2383
+ onUpgradeData = chunk => {
2384
+ upgradeBuffer = Buffer.concat([upgradeBuffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
2385
+ if (upgradeBuffer.length > 16 * 1024) {
2386
+ fail(431, 'Request Header Fields Too Large');
2387
+ return;
2388
+ }
2389
+ completeUpgrade();
2390
+ };
2391
+
2392
+ if (!completeUpgrade()) {
2393
+ socket.on('data', onUpgradeData);
2394
+ }
2395
+ }
2396
+
2397
+ function handleTcpAgentSocket(socket, firstChunk = null) {
2398
+ allSockets.add(socket);
2399
+ socket.setNoDelay(true);
2400
+ socket.setKeepAlive(true, heartbeatMs);
2401
+
2402
+ const state = {
2403
+ authenticated: false,
2404
+ device: null,
2405
+ buffer: Buffer.alloc(0),
2406
+ pendingBinaryFrame: null
2407
+ };
2408
+
2409
+ const helloTimer = setTimeout(() => {
2410
+ if (!state.authenticated) {
2411
+ writeJsonLine(socket, { type: 'error', error: 'hello-timeout' });
2412
+ socket.destroy();
2413
+ }
2414
+ }, 10000);
2415
+
2416
+ const processData = chunk => {
2417
+ const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
2418
+ state.buffer = state.buffer.length > 0
2419
+ ? Buffer.concat([state.buffer, incoming])
2420
+ : incoming;
2421
+
2422
+ while (!socket.destroyed) {
2423
+ if (state.pendingBinaryFrame) {
2424
+ const { header, byteLength } = state.pendingBinaryFrame;
2425
+ if (state.buffer.length < byteLength) {
2426
+ return;
2427
+ }
2428
+
2429
+ const framePayload = state.buffer.subarray(0, byteLength);
2430
+ state.buffer = state.buffer.subarray(byteLength);
2431
+ state.pendingBinaryFrame = null;
2432
+ handleAgentBinaryFrame(socket, state, header, framePayload);
2433
+ continue;
2434
+ }
2435
+
2436
+ const newlineIndex = state.buffer.indexOf(0x0a);
2437
+ if (newlineIndex < 0) {
2438
+ if (state.buffer.length > MAX_LINE_CHARS) {
2439
+ writeJsonLine(socket, { type: 'error', error: 'message-too-large' });
2440
+ socket.destroy();
2441
+ }
2442
+ return;
2443
+ }
2444
+
2445
+ const lineBuffer = state.buffer.subarray(0, newlineIndex);
2446
+ state.buffer = state.buffer.subarray(newlineIndex + 1);
2447
+
2448
+ try {
2449
+ const message = parseJsonLine(lineBuffer.toString('utf8'));
2450
+ if (message?.type === 'frame.binary') {
2451
+ const frameKind = safeString(message.frameKind || message.kind || message.frameType, 40).toLowerCase();
2452
+ const maxBytes = frameKind === 'thumbnail'
2453
+ ? MAX_THUMBNAIL_BINARY_BYTES
2454
+ : MAX_STREAM_BINARY_BYTES;
2455
+ const byteLength = Number(message.byteLength ?? message.payloadBytes ?? message.dataLength);
2456
+ if (!Number.isFinite(byteLength) || byteLength < 1 || byteLength > maxBytes) {
2457
+ writeJsonLine(socket, { type: 'error', error: 'invalid-binary-frame' });
2458
+ logWarn('remote', 'invalid binary frame header from agent.');
2459
+ continue;
2460
+ }
2461
+
2462
+ state.pendingBinaryFrame = {
2463
+ header: message,
2464
+ byteLength: Math.floor(byteLength)
2465
+ };
2466
+ continue;
2467
+ }
2468
+
2469
+ handleAgentMessage(socket, state, message);
2470
+ } catch (err) {
2471
+ writeJsonLine(socket, { type: 'error', error: 'invalid-json' });
2472
+ logWarn('remote', `invalid agent message: ${err?.message || err}`);
2473
+ }
2474
+ }
2475
+ };
2476
+
2477
+ socket.on('data', processData);
2478
+ if (firstChunk) {
2479
+ processData(firstChunk);
2480
+ }
2481
+
2482
+ socket.on('close', () => {
2483
+ allSockets.delete(socket);
2484
+ clearTimeout(helloTimer);
2485
+ detachSocket(socket);
2486
+ });
2487
+ socket.on('error', err => {
2488
+ allSockets.delete(socket);
2489
+ clearTimeout(helloTimer);
2490
+ detachSocket(socket, err?.message || 'socket-error');
2491
+ });
2492
+ }
2493
+
2494
+ function handleSocket(socket) {
2495
+ socket.once('data', chunk => {
2496
+ const firstChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
2497
+ if (isRemoteHubWebSocketUpgradeStart(firstChunk)) {
2498
+ handleWebSocketUpgradeSocket(socket, firstChunk);
2499
+ return;
2500
+ }
2501
+
2502
+ handleTcpAgentSocket(socket, firstChunk);
2503
+ });
2504
+
2505
+ socket.once('error', () => {
2506
+ // The transport-specific handler owns logging after the first byte.
2507
+ });
2508
+ }
2509
+
2510
+ async function start() {
2511
+ if (!enabled || started) {
2512
+ return getStatus({ includeSecrets: false });
2513
+ }
2514
+
2515
+ await new Promise((resolve, reject) => {
2516
+ server = net.createServer(handleSocket);
2517
+ server.once('error', err => {
2518
+ lastError = err?.message || String(err);
2519
+ server = null;
2520
+ reject(err);
2521
+ });
2522
+ server.listen(requestedPort, host, () => {
2523
+ started = true;
2524
+ boundPort = server.address()?.port || requestedPort;
2525
+ lastError = '';
2526
+ logEvent('remote', `RemoteHub listening on tcp://${host}:${boundPort}`, 'success');
2527
+ if (host === '0.0.0.0' || host === '::') {
2528
+ logWarn('remote', 'RemoteHub is externally reachable. Use a strong pairing token and trusted network.');
2529
+ }
2530
+ emitRemoteEvent('RemoteHubStarted', null);
2531
+ resolve();
2532
+ });
2533
+ });
2534
+
2535
+ return getStatus({ includeSecrets: false });
2536
+ }
2537
+
2538
+ async function close() {
2539
+ for (const device of devices.values()) {
2540
+ failAllPendingTasks(device, 'hub-shutdown');
2541
+ if (device.socket && !device.socket.destroyed) {
2542
+ writeJsonLine(device.socket, { type: 'disconnect', reason: 'hub-shutdown' });
2543
+ device.socket.destroy();
2544
+ }
2545
+ }
2546
+
2547
+ for (const socket of allSockets) {
2548
+ if (!socket.destroyed) {
2549
+ socket.destroy();
2550
+ }
2551
+ }
2552
+
2553
+ sockets.clear();
2554
+ if (!server) {
2555
+ started = false;
2556
+ return;
2557
+ }
2558
+
2559
+ await new Promise(resolve => {
2560
+ let settled = false;
2561
+ const settle = () => {
2562
+ if (settled) {
2563
+ return;
2564
+ }
2565
+
2566
+ settled = true;
2567
+ clearTimeout(closeTimer);
2568
+ resolve();
2569
+ };
2570
+
2571
+ const closeTimer = setTimeout(settle, 1000);
2572
+ closeTimer.unref?.();
2573
+
2574
+ try {
2575
+ server.close(settle);
2576
+ } catch {
2577
+ settle();
2578
+ }
2579
+ });
2580
+
2581
+ server = null;
2582
+ started = false;
2583
+ }
2584
+
2585
+ function disconnectDevice(deviceId, reason = 'manager-disconnect') {
2586
+ const device = devices.get(String(deviceId || ''));
2587
+ if (device?.synthetic === true) {
2588
+ device.connected = false;
2589
+ device.disconnectedAt = new Date().toISOString();
2590
+ device.lastDisconnectReason = reason;
2591
+ if (device.activeLiveStream) {
2592
+ device.activeLiveStream.active = false;
2593
+ device.activeLiveStream.stoppedAt = device.disconnectedAt;
2594
+ device.activeLiveStream.stopReason = reason;
2595
+ }
2596
+ failAllPendingTasks(device, 'device-disconnected');
2597
+ emitRemoteEvent('RemoteDeviceDisconnected', device, { reason, synthetic: true });
2598
+ return true;
2599
+ }
2600
+
2601
+ if (!device?.socket || device.socket.destroyed) {
2602
+ return false;
2603
+ }
2604
+
2605
+ writeJsonLine(device.socket, { type: 'disconnect', reason });
2606
+ device.socket.destroy();
2607
+ return true;
2608
+ }
2609
+
2610
+ function sendCommand(deviceId, command) {
2611
+ const device = devices.get(String(deviceId || ''));
2612
+ if (device?.synthetic === true && device.connected) {
2613
+ const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
2614
+ device.counters.commandsSent += 1;
2615
+ device.lastSeenAt = new Date().toISOString();
2616
+ if (command?.command === 'thumbnail.capture') {
2617
+ applySyntheticThumbnail(device, {
2618
+ commandId,
2619
+ payload: command?.payload || {}
2620
+ });
2621
+ }
2622
+ emitRemoteEvent('RemoteCommandQueued', device, {
2623
+ commandId,
2624
+ command: safeString(command?.command || 'ping', 80),
2625
+ synthetic: true
2626
+ });
2627
+ emitRemoteEvent('RemoteCommandResult', device, {
2628
+ commandId,
2629
+ result: { ok: true, synthetic: true },
2630
+ error: ''
2631
+ });
2632
+ return { ok: true, commandId, synthetic: true };
2633
+ }
2634
+
2635
+ if (!device?.socket || device.socket.destroyed || !device.connected) {
2636
+ return { ok: false, error: 'device-not-connected' };
2637
+ }
2638
+
2639
+ const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
2640
+ const payload = {
2641
+ type: 'command',
2642
+ commandId,
2643
+ command: safeString(command?.command || 'ping', 80),
2644
+ payload: command?.payload ?? null,
2645
+ issuedAt: new Date().toISOString()
2646
+ };
2647
+
2648
+ writeJsonLine(device.socket, payload);
2649
+ device.counters.commandsSent += 1;
2650
+ emitRemoteEvent('RemoteCommandQueued', device, {
2651
+ commandId,
2652
+ command: payload.command
2653
+ });
2654
+ return { ok: true, commandId };
2655
+ }
2656
+
2657
+ function sendInputControl(deviceId, input = {}) {
2658
+ const device = devices.get(String(deviceId || ''));
2659
+ if (!device) {
2660
+ return { ok: false, error: 'device-not-found' };
2661
+ }
2662
+
2663
+ if (!device.connected) {
2664
+ return { ok: false, error: 'device-not-connected' };
2665
+ }
2666
+
2667
+ if (!readCapabilityFlag(device.capabilities, 'control')) {
2668
+ return { ok: false, error: 'device-input-control-unavailable' };
2669
+ }
2670
+
2671
+ const normalized = normalizeRemoteInputEvent(input);
2672
+ if (!normalized.type) {
2673
+ return { ok: false, error: 'missing-input-type' };
2674
+ }
2675
+
2676
+ return sendCommand(deviceId, {
2677
+ command: 'input.control',
2678
+ payload: {
2679
+ ...normalized,
2680
+ issuedAt: normalized.issuedAt || new Date().toISOString()
2681
+ }
2682
+ });
2683
+ }
2684
+
2685
+ function requestAgentTask(deviceId, options = {}) {
2686
+ const device = devices.get(String(deviceId || ''));
2687
+ if (!device) {
2688
+ return { ok: false, error: 'device-not-found' };
2689
+ }
2690
+
2691
+ if (device?.synthetic === true && device.connected) {
2692
+ const instruction = safeText(options.instruction, MAX_AGENT_TASK_CHARS);
2693
+ if (!instruction) {
2694
+ return { ok: false, error: 'missing-instruction' };
2695
+ }
2696
+
2697
+ const now = new Date().toISOString();
2698
+ const approvalLevel = normalizeApprovalLevel(options.approvalLevel);
2699
+ if (approvalLevel === 'ai-assist' && !readCapabilityFlag(device.capabilities, 'aiAssist')) {
2700
+ return { ok: false, error: 'device-ai-assist-unavailable' };
2701
+ }
2702
+
2703
+ const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
2704
+ const taskId = safeString(options.taskId, 128) || crypto.randomUUID();
2705
+ const title = safeString(options.title, 120)
2706
+ || safeString(instruction.split(/\r?\n/)[0], 120)
2707
+ || 'Remote task';
2708
+ const task = {
2709
+ batchId: safeString(options.batchId, 128),
2710
+ taskId,
2711
+ commandId,
2712
+ title,
2713
+ instructionPreview: safeText(instruction, 320),
2714
+ status: 'completed',
2715
+ approvalLevel,
2716
+ requestedAt: now,
2717
+ sentAt: now,
2718
+ updatedAt: now,
2719
+ completedAt: now,
2720
+ error: '',
2721
+ resultKind: approvalLevel === 'ai-assist' ? 'synthetic-ai-assist' : 'synthetic-agent-task',
2722
+ resultModel: approvalLevel === 'ai-assist'
2723
+ ? safeString(options.model, 120) || 'synthetic-ai'
2724
+ : '',
2725
+ resultResponseId: approvalLevel === 'ai-assist'
2726
+ ? `synthetic-response-${taskId}`
2727
+ : '',
2728
+ resultSummary: approvalLevel === 'ai-assist'
2729
+ ? `Synthetic AI assist completed for ${device.deviceName}.`
2730
+ : `Synthetic task accepted by ${device.deviceName}.`
2731
+ };
2732
+
2733
+ device.counters.commandsSent += 1;
2734
+ device.counters.commandResultsReceived += 1;
2735
+ device.counters.tasksQueued += 1;
2736
+ device.counters.taskResultsReceived += 1;
2737
+ device.lastSeenAt = now;
2738
+ rememberDeviceTask(device, task);
2739
+ device.pendingTaskCommands.delete(commandId);
2740
+ syncTaskBatchFromTask(device, task);
2741
+ emitRemoteEvent('RemoteTaskQueued', device, {
2742
+ commandId,
2743
+ taskId,
2744
+ title,
2745
+ approvalLevel,
2746
+ synthetic: true
2747
+ });
2748
+ emitRemoteEvent('RemoteTaskResult', device, {
2749
+ commandId,
2750
+ taskId,
2751
+ status: task.status,
2752
+ error: '',
2753
+ synthetic: true
2754
+ });
2755
+ return { ok: true, commandId, taskId, approvalLevel, synthetic: true };
2756
+ }
2757
+
2758
+ if (!device?.socket || device.socket.destroyed || !device.connected) {
2759
+ return { ok: false, error: 'device-not-connected' };
2760
+ }
2761
+
2762
+ const instruction = safeText(options.instruction, MAX_AGENT_TASK_CHARS);
2763
+ if (!instruction) {
2764
+ return { ok: false, error: 'missing-instruction' };
2765
+ }
2766
+
2767
+ const now = new Date().toISOString();
2768
+ const approvalLevel = normalizeApprovalLevel(options.approvalLevel);
2769
+ if (approvalLevel === 'ai-assist' && !readCapabilityFlag(device.capabilities, 'aiAssist')) {
2770
+ return { ok: false, error: 'device-ai-assist-unavailable' };
2771
+ }
2772
+
2773
+ const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
2774
+ const taskId = safeString(options.taskId, 128) || crypto.randomUUID();
2775
+ const title = safeString(options.title, 120)
2776
+ || safeString(instruction.split(/\r?\n/)[0], 120)
2777
+ || 'Remote task';
2778
+ const task = {
2779
+ batchId: safeString(options.batchId, 128),
2780
+ taskId,
2781
+ commandId,
2782
+ title,
2783
+ instructionPreview: safeText(instruction, 320),
2784
+ status: 'queued',
2785
+ approvalLevel,
2786
+ requestedAt: now,
2787
+ sentAt: now,
2788
+ updatedAt: now,
2789
+ completedAt: '',
2790
+ error: '',
2791
+ resultKind: '',
2792
+ resultModel: '',
2793
+ resultResponseId: '',
2794
+ resultSummary: ''
2795
+ };
2796
+
2797
+ const sent = writeJsonLine(device.socket, {
2798
+ type: 'command',
2799
+ commandId,
2800
+ command: 'agent.task',
2801
+ payload: {
2802
+ taskId,
2803
+ title,
2804
+ instruction,
2805
+ approvalLevel,
2806
+ model: safeString(options.model, 120),
2807
+ requestedAt: now
2808
+ },
2809
+ issuedAt: now
2810
+ });
2811
+
2812
+ if (!sent) {
2813
+ return { ok: false, error: 'device-not-connected' };
2814
+ }
2815
+
2816
+ device.counters.commandsSent += 1;
2817
+ device.counters.tasksQueued += 1;
2818
+ rememberDeviceTask(device, task);
2819
+ syncTaskBatchFromTask(device, task);
2820
+ scheduleTaskTimeout(device, task);
2821
+ emitRemoteEvent('RemoteTaskQueued', device, {
2822
+ commandId,
2823
+ taskId,
2824
+ title,
2825
+ approvalLevel
2826
+ });
2827
+ return { ok: true, commandId, taskId, approvalLevel };
2828
+ }
2829
+
2830
+ function requestAgentTaskBatch(deviceIds, options = {}) {
2831
+ const targets = [...new Set((deviceIds || [])
2832
+ .map(deviceId => safeString(deviceId, 128))
2833
+ .filter(Boolean))]
2834
+ .slice(0, 500);
2835
+
2836
+ if (targets.length === 0) {
2837
+ return {
2838
+ ok: false,
2839
+ error: 'no-target-devices',
2840
+ total: 0,
2841
+ queued: 0,
2842
+ approvalLevel: normalizeApprovalLevel(options.approvalLevel),
2843
+ results: []
2844
+ };
2845
+ }
2846
+
2847
+ const batch = beginTaskBatch(targets, options);
2848
+ const results = targets.map(deviceId => {
2849
+ const result = requestAgentTask(deviceId, {
2850
+ ...options,
2851
+ batchId: batch.batchId
2852
+ });
2853
+
2854
+ if (result.ok !== true) {
2855
+ syncTaskBatchDispatchFailure(batch.batchId, deviceId, result.error, result);
2856
+ }
2857
+
2858
+ return {
2859
+ deviceId,
2860
+ ...result
2861
+ };
2862
+ });
2863
+
2864
+ const queued = results.filter(result => result.ok === true).length;
2865
+ const updatedBatch = taskBatches.get(batch.batchId) || batch;
2866
+ recalculateTaskBatch(updatedBatch);
2867
+ return {
2868
+ ok: queued > 0,
2869
+ batchId: batch.batchId,
2870
+ total: targets.length,
2871
+ queued,
2872
+ approvalLevel: batch.approvalLevel,
2873
+ results,
2874
+ batch: serializeTaskBatch(updatedBatch),
2875
+ error: queued > 0 ? undefined : 'no-task-queued'
2876
+ };
2877
+ }
2878
+
2879
+ function requestThumbnail(deviceId, options = {}) {
2880
+ return sendCommand(deviceId, {
2881
+ command: 'thumbnail.capture',
2882
+ commandId: safeString(options.commandId, 128) || crypto.randomUUID(),
2883
+ payload: {
2884
+ streamId: safeString(options.streamId, 128) || `thumb-${Date.now()}`,
2885
+ maxWidth: clampNumber(options.maxWidth, 160, 1920, 360),
2886
+ maxHeight: clampNumber(options.maxHeight, 90, 1080, 220),
2887
+ quality: clampNumber(options.quality, 20, 95, 55),
2888
+ requestedAt: new Date().toISOString()
2889
+ }
2890
+ });
2891
+ }
2892
+
2893
+ function startLiveStream(deviceId, options = {}) {
2894
+ const device = devices.get(String(deviceId || ''));
2895
+ if (device?.synthetic === true && device.connected) {
2896
+ if (!readCapabilityFlag(device.capabilities, 'liveStream')) {
2897
+ return { ok: false, error: 'device-live-stream-unavailable' };
2898
+ }
2899
+
2900
+ const now = new Date().toISOString();
2901
+ const streamId = safeString(options.streamId, 128) || `live-${Date.now()}`;
2902
+ const fps = clampNumber(options.fps, 1, 24, 20);
2903
+ const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
2904
+ device.counters.commandsSent += 1;
2905
+ device.activeLiveStream = {
2906
+ streamId,
2907
+ commandId,
2908
+ active: true,
2909
+ mode: 'remote-fast',
2910
+ fps,
2911
+ startedAt: now,
2912
+ stoppedAt: '',
2913
+ stopReason: '',
2914
+ lastFrameAt: '',
2915
+ lastFrameSeq: 0,
2916
+ framesReceived: 0
2917
+ };
2918
+ device.counters.liveStreamsStarted += 1;
2919
+ applySyntheticLiveFrame(device, streamId, {
2920
+ commandId,
2921
+ maxWidth: options.maxWidth,
2922
+ maxHeight: options.maxHeight,
2923
+ fps
2924
+ });
2925
+ emitRemoteEvent('RemoteLiveStreamStarted', device, {
2926
+ streamId,
2927
+ commandId,
2928
+ fps,
2929
+ synthetic: true
2930
+ });
2931
+ return { ok: true, commandId, streamId, fps, synthetic: true };
2932
+ }
2933
+
2934
+ if (!device?.socket || device.socket.destroyed || !device.connected) {
2935
+ return { ok: false, error: 'device-not-connected' };
2936
+ }
2937
+ if (!readCapabilityFlag(device.capabilities, 'liveStream')) {
2938
+ return { ok: false, error: 'device-live-stream-unavailable' };
2939
+ }
2940
+
2941
+ const now = new Date().toISOString();
2942
+ const streamId = safeString(options.streamId, 128) || `live-${Date.now()}`;
2943
+ const fps = clampNumber(options.fps, 1, 24, 12);
2944
+ const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
2945
+ const result = sendCommand(deviceId, {
2946
+ command: 'stream.start',
2947
+ commandId,
2948
+ payload: {
2949
+ streamId,
2950
+ mode: safeString(options.mode, 80) || 'remote-fast',
2951
+ fps,
2952
+ maxWidth: clampNumber(options.maxWidth, 320, 2560, 960),
2953
+ maxHeight: clampNumber(options.maxHeight, 180, 1440, 540),
2954
+ quality: clampNumber(options.quality, 20, 95, 60),
2955
+ requestedAt: now
2956
+ }
2957
+ });
2958
+
2959
+ if (!result.ok) {
2960
+ return result;
2961
+ }
2962
+
2963
+ device.activeLiveStream = {
2964
+ streamId,
2965
+ commandId,
2966
+ active: true,
2967
+ mode: 'remote-fast',
2968
+ fps,
2969
+ startedAt: now,
2970
+ stoppedAt: '',
2971
+ stopReason: '',
2972
+ lastFrameAt: '',
2973
+ lastFrameSeq: 0,
2974
+ framesReceived: 0
2975
+ };
2976
+ device.counters.liveStreamsStarted += 1;
2977
+ emitRemoteEvent('RemoteLiveStreamStarted', device, {
2978
+ streamId,
2979
+ commandId,
2980
+ fps
2981
+ });
2982
+
2983
+ return { ok: true, commandId, streamId, fps };
2984
+ }
2985
+
2986
+ function stopLiveStream(deviceId, options = {}) {
2987
+ const device = devices.get(String(deviceId || ''));
2988
+ if (device?.synthetic === true && device.connected) {
2989
+ const streamId = safeString(options.streamId, 128) || device.activeLiveStream?.streamId || '';
2990
+ const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
2991
+ device.counters.commandsSent += 1;
2992
+ if (device.activeLiveStream && (!streamId || device.activeLiveStream.streamId === streamId)) {
2993
+ device.activeLiveStream.active = false;
2994
+ device.activeLiveStream.stoppedAt = new Date().toISOString();
2995
+ device.activeLiveStream.stopReason = 'manager-request';
2996
+ }
2997
+ device.counters.liveStreamsStopped += 1;
2998
+ emitRemoteEvent('RemoteLiveStreamStopped', device, {
2999
+ streamId,
3000
+ commandId,
3001
+ synthetic: true
3002
+ });
3003
+ return { ok: true, commandId, streamId, synthetic: true };
3004
+ }
3005
+
3006
+ if (!device?.socket || device.socket.destroyed || !device.connected) {
3007
+ return { ok: false, error: 'device-not-connected' };
3008
+ }
3009
+
3010
+ const streamId = safeString(options.streamId, 128) || device.activeLiveStream?.streamId || '';
3011
+ const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
3012
+ const result = sendCommand(deviceId, {
3013
+ command: 'stream.stop',
3014
+ commandId,
3015
+ payload: {
3016
+ streamId,
3017
+ requestedAt: new Date().toISOString()
3018
+ }
3019
+ });
3020
+
3021
+ if (!result.ok) {
3022
+ return result;
3023
+ }
3024
+
3025
+ if (device.activeLiveStream && (!streamId || device.activeLiveStream.streamId === streamId)) {
3026
+ device.activeLiveStream.active = false;
3027
+ device.activeLiveStream.stoppedAt = new Date().toISOString();
3028
+ device.activeLiveStream.stopReason = 'manager-request';
3029
+ }
3030
+ device.counters.liveStreamsStopped += 1;
3031
+ emitRemoteEvent('RemoteLiveStreamStopped', device, {
3032
+ streamId,
3033
+ commandId
3034
+ });
3035
+
3036
+ return { ok: true, commandId, streamId };
3037
+ }
3038
+
3039
+ function getDeviceLiveFrame(deviceId, options = {}) {
3040
+ const device = devices.get(String(deviceId || ''));
3041
+ return serializeRemoteFrame(device?.latestLiveFrame, device?.deviceId, 'live', {
3042
+ includeDataUrl: options.includeDataUrl === true
3043
+ });
3044
+ }
3045
+
3046
+ function getDeviceThumbnail(deviceId, options = {}) {
3047
+ const device = devices.get(String(deviceId || ''));
3048
+ return serializeRemoteFrame(device?.latestThumbnail, device?.deviceId, 'thumbnail', {
3049
+ includeDataUrl: options.includeDataUrl === true
3050
+ });
3051
+ }
3052
+
3053
+ function getFramePayload(deviceId, frameKind, options = {}) {
3054
+ const device = devices.get(String(deviceId || ''));
3055
+ if (!device) {
3056
+ return null;
3057
+ }
3058
+
3059
+ const kind = frameKind === 'thumbnail' ? 'thumbnail' : 'live';
3060
+ const frame = findRecentFramePayload(device, kind, options);
3061
+ if (!frame) {
3062
+ return null;
3063
+ }
3064
+
3065
+ return {
3066
+ frame: serializeRemoteFrame(frame, device.deviceId, kind, { includeDataUrl: false }),
3067
+ payload: frame.payload,
3068
+ mimeType: safeString(frame.mimeType || frame.format || 'application/octet-stream', 120) || 'application/octet-stream',
3069
+ byteLength: frame.payload.length
3070
+ };
3071
+ }
3072
+
3073
+ return {
3074
+ start,
3075
+ close,
3076
+ getStatus,
3077
+ listDevices,
3078
+ listTaskBatches,
3079
+ getLatestTaskBatch,
3080
+ listDeviceFrames,
3081
+ disconnectDevice,
3082
+ sendCommand,
3083
+ sendInputControl,
3084
+ requestAgentTask,
3085
+ requestAgentTaskBatch,
3086
+ setHostTarget,
3087
+ requestThumbnail,
3088
+ startLiveStream,
3089
+ stopLiveStream,
3090
+ getDeviceLiveFrame,
3091
+ getDeviceThumbnail,
3092
+ getFramePayload,
3093
+ seedSyntheticFleet,
3094
+ clearSyntheticFleet,
3095
+ getPairToken: () => pairToken
3096
+ };
3097
+ }
3098
+
3099
+ export function getDefaultRemoteAgentDeviceInfo() {
3100
+ return {
3101
+ hostname: os.hostname(),
3102
+ platform: os.platform(),
3103
+ arch: os.arch(),
3104
+ pid: process.pid
3105
+ };
3106
+ }