livedesk 0.1.447 → 0.1.449

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.
@@ -3,17 +3,12 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
 
5
5
  export const AGENT_PROVIDER_CODEX = 'codex';
6
- export const AGENT_PROVIDER_OPENCODE_GO = 'opencode-go';
7
6
  export const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_CODEX;
8
- export const DEFAULT_AGENT_BASE_URL = 'https://opencode.ai/zen/go/v1';
9
- export const DEFAULT_CODEX_MODEL_ID = '';
10
7
  export const DEFAULT_AGENT_WORKSPACE = path.join(os.homedir(), '.livedesk', 'agent-workspace');
11
8
  export const DEFAULT_AGENT_SETTINGS = Object.freeze({
12
- settingsVersion: 2,
9
+ settingsVersion: 4,
13
10
  enabled: true,
14
11
  provider: DEFAULT_AGENT_PROVIDER,
15
- codexModelId: DEFAULT_CODEX_MODEL_ID,
16
- codexReasoningEffort: 'medium',
17
12
  codexSandboxMode: 'read-only',
18
13
  codexApprovalPolicy: 'never',
19
14
  codexWorkingDirectory: DEFAULT_AGENT_WORKSPACE,
@@ -24,44 +19,9 @@ export const DEFAULT_AGENT_SETTINGS = Object.freeze({
24
19
  codexShowDetailedEvents: true,
25
20
  codexNetworkAccessEnabled: false,
26
21
  codexWebSearchMode: 'disabled',
27
- deterministicParserFirst: true,
28
- generateSummary: true,
29
- fallbackSummary: true,
30
- includeRawDeviceDetails: false,
31
- maxConcurrentRequests: 2,
32
- baseUrl: DEFAULT_AGENT_BASE_URL,
33
- defaultModelId: 'deepseek-v4-flash',
34
- planModelId: '',
35
- summaryModelId: '',
36
- reasoningLevel: 'auto',
37
- planTemperature: 0.1,
38
- summaryTemperature: 0.2,
39
- topP: 1,
40
- planMaxOutputTokens: 1024,
41
- summaryMaxOutputTokens: 1024,
42
- planTimeoutMs: 60000,
43
- summaryTimeoutMs: 60000,
44
- retryCount: 1,
45
- retryDelayMs: 1000,
46
- streamSummary: false
22
+ maxConcurrentRequests: 2
47
23
  });
48
24
 
49
- const PROVIDERS = new Set([AGENT_PROVIDER_CODEX, AGENT_PROVIDER_OPENCODE_GO]);
50
- const REASONING_EFFORTS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh']);
51
- const SANDBOX_MODES = new Set(['read-only', 'workspace-write', 'danger-full-access']);
52
- const APPROVAL_POLICIES = new Set(['never', 'on-request', 'on-failure', 'untrusted']);
53
- const WEB_SEARCH_MODES = new Set(['disabled', 'cached', 'live']);
54
- const LEGACY_REASONING_LEVELS = new Set(['auto', 'low', 'medium', 'high']);
55
-
56
- export class AgentSettingsError extends Error {
57
- constructor(code, message) {
58
- super(message);
59
- this.name = 'AgentSettingsError';
60
- this.code = code;
61
- this.status = 400;
62
- }
63
- }
64
-
65
25
  function booleanValue(value, fallback) {
66
26
  return typeof value === 'boolean' ? value : fallback;
67
27
  }
@@ -73,31 +33,7 @@ function numberValue(value, min, max, fallback, integer = false) {
73
33
  return integer ? Math.round(clamped) : clamped;
74
34
  }
75
35
 
76
- function stringValue(value, fallback, maxLength = 240) {
77
- if (typeof value !== 'string') return fallback;
78
- return value.trim().slice(0, maxLength);
79
- }
80
-
81
- function enumValue(value, allowed, fallback, code) {
82
- const normalized = stringValue(value, fallback, 80).toLowerCase();
83
- if (!allowed.has(normalized)) throw new AgentSettingsError(code, `Invalid agent setting: ${normalized}.`);
84
- return normalized;
85
- }
86
-
87
- function normalizeBaseUrl(value) {
88
- const candidate = stringValue(value, DEFAULT_AGENT_SETTINGS.baseUrl, 500).replace(/\/+$/, '');
89
- try {
90
- const parsed = new URL(candidate);
91
- if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname) throw new Error('invalid protocol');
92
- parsed.hash = '';
93
- parsed.search = '';
94
- return parsed.toString().replace(/\/+$/, '');
95
- } catch {
96
- throw new AgentSettingsError('agent-invalid-base-url', 'Base URL must be a valid HTTP or HTTPS URL.');
97
- }
98
- }
99
-
100
- function normalizeWorkingDirectory(value) {
36
+ function normalizeWorkingDirectory() {
101
37
  // The agent runtime owns this directory. Do not allow a browser/API caller
102
38
  // to move Codex into a user project or an arbitrary filesystem root.
103
39
  return DEFAULT_AGENT_WORKSPACE;
@@ -105,25 +41,17 @@ function normalizeWorkingDirectory(value) {
105
41
 
106
42
  export function normalizeAgentSettings(value = {}) {
107
43
  const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
108
- // The pre-Codex settings file was written by LiveDesk itself and had no
109
- // migration marker. Keep the legacy values but move that first-run config
110
- // to the new Codex default.
111
- const migratingLegacy = Number(source.settingsVersion || 0) < 2 && source.provider === AGENT_PROVIDER_OPENCODE_GO;
112
- const provider = migratingLegacy
113
- ? DEFAULT_AGENT_PROVIDER
114
- : enumValue(source.provider, PROVIDERS, DEFAULT_AGENT_PROVIDER, 'agent-unsupported-provider');
115
- const reasoningLevel = enumValue(source.reasoningLevel, LEGACY_REASONING_LEVELS, DEFAULT_AGENT_SETTINGS.reasoningLevel, 'agent-invalid-reasoning-level');
116
44
  return {
117
- settingsVersion: 2,
45
+ settingsVersion: 4,
118
46
  enabled: booleanValue(source.enabled, DEFAULT_AGENT_SETTINGS.enabled),
119
- provider,
120
- codexModelId: stringValue(source.codexModelId, DEFAULT_AGENT_SETTINGS.codexModelId, 160),
121
- codexReasoningEffort: enumValue(source.codexReasoningEffort, REASONING_EFFORTS, DEFAULT_AGENT_SETTINGS.codexReasoningEffort, 'agent-invalid-reasoning-effort'),
47
+ // LiveDesk has one Agent runtime. Old provider values are deliberately
48
+ // normalized to Codex so an upgrade cannot reactivate a retired provider.
49
+ provider: AGENT_PROVIDER_CODEX,
122
50
  // These are safety settings, not user-controlled execution toggles. They
123
51
  // are accepted for compatibility but always normalized to the safe floor.
124
52
  codexSandboxMode: 'read-only',
125
53
  codexApprovalPolicy: 'never',
126
- codexWorkingDirectory: normalizeWorkingDirectory(source.codexWorkingDirectory),
54
+ codexWorkingDirectory: normalizeWorkingDirectory(),
127
55
  codexTaskTimeoutMs: numberValue(source.codexTaskTimeoutMs, 30000, 1800000, DEFAULT_AGENT_SETTINGS.codexTaskTimeoutMs, true),
128
56
  codexMaxTurns: numberValue(source.codexMaxTurns, 1, 40, DEFAULT_AGENT_SETTINGS.codexMaxTurns, true),
129
57
  codexMaxToolCalls: numberValue(source.codexMaxToolCalls, 1, 100, DEFAULT_AGENT_SETTINGS.codexMaxToolCalls, true),
@@ -131,26 +59,7 @@ export function normalizeAgentSettings(value = {}) {
131
59
  codexShowDetailedEvents: booleanValue(source.codexShowDetailedEvents, DEFAULT_AGENT_SETTINGS.codexShowDetailedEvents),
132
60
  codexNetworkAccessEnabled: false,
133
61
  codexWebSearchMode: 'disabled',
134
- deterministicParserFirst: booleanValue(source.deterministicParserFirst, DEFAULT_AGENT_SETTINGS.deterministicParserFirst),
135
- generateSummary: booleanValue(source.generateSummary, DEFAULT_AGENT_SETTINGS.generateSummary),
136
- fallbackSummary: booleanValue(source.fallbackSummary, DEFAULT_AGENT_SETTINGS.fallbackSummary),
137
- includeRawDeviceDetails: booleanValue(source.includeRawDeviceDetails, DEFAULT_AGENT_SETTINGS.includeRawDeviceDetails),
138
- maxConcurrentRequests: numberValue(source.maxConcurrentRequests, 1, 8, DEFAULT_AGENT_SETTINGS.maxConcurrentRequests, true),
139
- baseUrl: normalizeBaseUrl(source.baseUrl ?? DEFAULT_AGENT_SETTINGS.baseUrl),
140
- defaultModelId: stringValue(source.defaultModelId, DEFAULT_AGENT_SETTINGS.defaultModelId, 160),
141
- planModelId: stringValue(source.planModelId, DEFAULT_AGENT_SETTINGS.planModelId, 160),
142
- summaryModelId: stringValue(source.summaryModelId, DEFAULT_AGENT_SETTINGS.summaryModelId, 160),
143
- reasoningLevel,
144
- planTemperature: numberValue(source.planTemperature, 0, 2, DEFAULT_AGENT_SETTINGS.planTemperature),
145
- summaryTemperature: numberValue(source.summaryTemperature, 0, 2, DEFAULT_AGENT_SETTINGS.summaryTemperature),
146
- topP: numberValue(source.topP, 0, 1, DEFAULT_AGENT_SETTINGS.topP),
147
- planMaxOutputTokens: numberValue(source.planMaxOutputTokens, 128, 16384, DEFAULT_AGENT_SETTINGS.planMaxOutputTokens, true),
148
- summaryMaxOutputTokens: numberValue(source.summaryMaxOutputTokens, 128, 16384, DEFAULT_AGENT_SETTINGS.summaryMaxOutputTokens, true),
149
- planTimeoutMs: numberValue(source.planTimeoutMs, 5000, 180000, DEFAULT_AGENT_SETTINGS.planTimeoutMs, true),
150
- summaryTimeoutMs: numberValue(source.summaryTimeoutMs, 5000, 180000, DEFAULT_AGENT_SETTINGS.summaryTimeoutMs, true),
151
- retryCount: numberValue(source.retryCount, 0, 4, DEFAULT_AGENT_SETTINGS.retryCount, true),
152
- retryDelayMs: numberValue(source.retryDelayMs, 100, 10000, DEFAULT_AGENT_SETTINGS.retryDelayMs, true),
153
- streamSummary: booleanValue(source.streamSummary, DEFAULT_AGENT_SETTINGS.streamSummary)
62
+ maxConcurrentRequests: numberValue(source.maxConcurrentRequests, 1, 8, DEFAULT_AGENT_SETTINGS.maxConcurrentRequests, true)
154
63
  };
155
64
  }
156
65
 
@@ -79,7 +79,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
79
79
  },
80
80
  {
81
81
  name: 'livedesk.list_processes',
82
- description: 'Read process status from the selected Clients. Use processName for a named process such as ComfyUI.',
82
+ description: 'Read process status from the selected Clients. Use processName when the request names a specific process.',
83
83
  category: 'read',
84
84
  readOnly: true,
85
85
  mutating: false,
@@ -66,15 +66,13 @@ function normalizeCodexError(error) {
66
66
  return new AgentProviderError('codex-run-failed', message, { status: 502, retryable: true });
67
67
  }
68
68
 
69
- function codexThreadOptions(settings, workspace) {
70
- return {
71
- model: settings.codexModelId || undefined,
72
- sandboxMode: 'read-only',
73
- approvalPolicy: 'never',
74
- workingDirectory: workspace,
75
- skipGitRepoCheck: true,
76
- modelReasoningEffort: settings.codexReasoningEffort,
77
- networkAccessEnabled: false,
69
+ function codexThreadOptions(workspace) {
70
+ return {
71
+ sandboxMode: 'read-only',
72
+ approvalPolicy: 'never',
73
+ workingDirectory: workspace,
74
+ skipGitRepoCheck: true,
75
+ networkAccessEnabled: false,
78
76
  webSearchMode: 'disabled',
79
77
  webSearchEnabled: false,
80
78
  additionalDirectories: []
@@ -417,10 +415,10 @@ export function createCodexAgentRuntime({
417
415
  const controller = new AbortController();
418
416
  const timer = setTimeout(() => controller.abort(), Math.min(30000, settings.codexTaskTimeoutMs));
419
417
  try {
420
- const thread = codex.startThread(codexThreadOptions(settings, workspace));
421
- const turn = await thread.run('Reply with the single word READY. Do not use tools.', { signal: controller.signal });
422
- if (!String(turn.finalResponse || '').trim()) throw new AgentProviderError('codex-empty-response', 'Codex returned an empty response.', { status: 502 });
423
- return { ok: true, modelId: settings.codexModelId || 'Codex default', modelName: settings.codexModelId || 'Codex default', latencyMs: Date.now() - startedAt, threadId: thread.id || '' };
418
+ const thread = codex.startThread(codexThreadOptions(workspace));
419
+ const turn = await thread.run('Reply with the single word READY. Do not use tools.', { signal: controller.signal });
420
+ if (!String(turn.finalResponse || '').trim()) throw new AgentProviderError('codex-empty-response', 'Codex returned an empty response.', { status: 502 });
421
+ return { ok: true, latencyMs: Date.now() - startedAt, threadId: thread.id || '' };
424
422
  } catch (error) {
425
423
  throw normalizeCodexError(error);
426
424
  } finally {
@@ -472,7 +470,7 @@ export function createCodexAgentRuntime({
472
470
  'Never use arbitrary MCP servers, change permissions, forge approvals, request credentials, or invent a tool result.',
473
471
  `The Hub has fixed this run to permission mode ${permissionPolicy?.mode || 'ask'} and enforces the policy independently of your instructions.`,
474
472
  'Use only the selected connected device IDs below. If the Hub asks for user approval, wait for that approval result and do not work around it.',
475
- 'You may perform multiple safe read-only checks when the request requires a sequence. For example, find Clients without ComfyUI, then check the service status only on those Clients.',
473
+ 'You may perform multiple safe read-only checks when the request requires a sequence. For example, find Clients missing a named process, then check a related service only on those Clients.',
476
474
  `Selected device IDs: ${JSON.stringify(deviceIds)}`,
477
475
  'Return a concise Korean or English summary grounded only in tool results. Do not invent results.',
478
476
  `User request: ${safeText(instruction, 4000)}`
@@ -531,7 +529,7 @@ export function createCodexAgentRuntime({
531
529
  if (run.abortReason === 'timeout') throw codexAbortError('codex-run-timeout', 'The Codex task exceeded its timeout.');
532
530
  throw Object.assign(new Error('The Codex task was cancelled.'), { name: 'AbortError' });
533
531
  }
534
- const threadOptions = codexThreadOptions(settings, workspace);
532
+ const threadOptions = codexThreadOptions(workspace);
535
533
  run.status = 'running';
536
534
  run.updatedAt = new Date().toISOString();
537
535
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -649,7 +647,7 @@ export function createCodexAgentRuntime({
649
647
  const deviceIds = safeAgentDeviceIds(input.deviceIds);
650
648
  if (deviceIds.length === 0) throw new AgentProviderError('agent-no-target-devices', 'Select at least one connected device.', { status: 400 });
651
649
  const settings = await settingsStore.get();
652
- if (!settings.enabled) throw new AgentProviderError('agent-ai-disabled', 'Agent AI is disabled in Settings.', { status: 409 });
650
+ if (!settings.enabled) throw new AgentProviderError('agent-ai-disabled', 'Codex Agent is disabled in Settings.', { status: 409 });
653
651
  assertSecurityConfiguration();
654
652
  pruneRuns();
655
653
  if (runs.size >= MAX_RUNS) throw new AgentProviderError('agent-run-limit', 'The LiveDesk Agent run history is full.', { status: 429, retryable: true });
@@ -644,15 +644,27 @@ function safeTaskData(value) {
644
644
  }
645
645
  }
646
646
 
647
- function normalizeApprovalLevel(value) {
648
- const level = safeString(value, 80).toLowerCase();
649
- if (level === 'read-only') {
650
- return 'read-only';
651
- }
652
- return level === 'ai-assist' ? 'ai-assist' : 'task-only';
653
- }
654
-
655
- const SUPPORTED_AGENT_OPERATIONS = new Set([
647
+ function normalizeApprovalLevel(value) {
648
+ const level = safeString(value, 80).toLowerCase();
649
+ if (level === 'read-only') {
650
+ return 'read-only';
651
+ }
652
+ return 'task-only';
653
+ }
654
+
655
+ const RETIRED_AGENT_TASK_ERROR = 'agent-ai-assist-retired';
656
+
657
+ function getRetiredAgentTaskError(options = {}) {
658
+ const approvalLevel = safeString(options?.approvalLevel, 80).toLowerCase();
659
+ const hasModelSelection = options
660
+ && typeof options === 'object'
661
+ && Object.prototype.hasOwnProperty.call(options, 'model');
662
+ return approvalLevel === 'ai-assist' || hasModelSelection
663
+ ? RETIRED_AGENT_TASK_ERROR
664
+ : '';
665
+ }
666
+
667
+ const SUPPORTED_AGENT_OPERATIONS = new Set([
656
668
  'system.health',
657
669
  'gpu.status',
658
670
  'disk.status',
@@ -2586,13 +2598,12 @@ export function createRemoteHub(options = {}) {
2586
2598
  const count = clampNumber(options.count, 1, MAX_SYNTHETIC_DEVICES, 250);
2587
2599
  const connectedRatio = Math.max(0, Math.min(1, Number(options.connectedRatio ?? 0.88)));
2588
2600
  const thumbnailRatio = Math.max(0, Math.min(1, Number(options.thumbnailRatio ?? 0.7)));
2589
- const aiAssistRatio = Math.max(0, Math.min(1, Number(options.aiAssistRatio ?? 0.35)));
2590
- const liveCount = clampNumber(options.liveCount, 0, Math.min(count, 24), Math.min(6, count));
2601
+ const liveCount = clampNumber(options.liveCount, 0, Math.min(count, 24), Math.min(6, count));
2591
2602
  const replace = options.replace !== false;
2592
2603
  const now = new Date();
2593
2604
  const platforms = ['win32', 'linux', 'darwin'];
2594
2605
  const machineKinds = ['Mac mini', 'Mini PC', 'Client', 'Render node', 'Dev box'];
2595
- const workloadRoles = ['LLM worker', 'Image worker', 'Video worker', 'Idle reserve', 'Build worker'];
2606
+ const workloadRoles = ['Office computer', 'Media workstation', 'Development machine', 'Idle machine', 'Build machine'];
2596
2607
  const gpuNames = ['Apple M-series GPU', 'NVIDIA RTX local', 'Radeon Pro', 'Intel Arc', 'Integrated GPU'];
2597
2608
  const npuNames = ['Apple Neural Engine', 'Ryzen AI NPU', 'Intel AI Boost', 'Qualcomm Hexagon', 'NPU not present'];
2598
2609
 
@@ -2601,15 +2612,13 @@ export function createRemoteHub(options = {}) {
2601
2612
  }
2602
2613
 
2603
2614
  let connected = 0;
2604
- let aiAssist = 0;
2605
- let live = 0;
2615
+ let live = 0;
2606
2616
  for (let index = 0; index < count; index += 1) {
2607
2617
  const ordinal = index + 1;
2608
2618
  const deviceId = `synthetic-${String(ordinal).padStart(4, '0')}`;
2609
2619
  const isConnected = index / count < connectedRatio;
2610
2620
  const hasThumbnail = index / count < thumbnailRatio;
2611
- const hasAiAssist = index / count < aiAssistRatio;
2612
- const isLive = isConnected && index < liveCount;
2621
+ const isLive = isConnected && index < liveCount;
2613
2622
  const seenAt = new Date(now.getTime() - index * 1350).toISOString();
2614
2623
  const connectedAt = new Date(now.getTime() - (index + 8) * 60000).toISOString();
2615
2624
  const platform = platforms[index % platforms.length];
@@ -2631,9 +2640,7 @@ export function createRemoteHub(options = {}) {
2631
2640
  const npuTops = index % 5 === 4 ? 0 : platform === 'darwin' ? 38 : index % 2 === 0 ? 45 : 16;
2632
2641
  const machineKind = machineKinds[index % machineKinds.length];
2633
2642
  const workloadRole = workloadRoles[index % workloadRoles.length];
2634
- const toolBusy = index % 9 === 0;
2635
- const toolOnline = index % 4 !== 3;
2636
- const taskStatus = index % 17 === 0
2643
+ const taskStatus = index % 17 === 0
2637
2644
  ? 'failed'
2638
2645
  : index % 5 === 0
2639
2646
  ? 'completed'
@@ -2647,18 +2654,16 @@ export function createRemoteHub(options = {}) {
2647
2654
  title: taskStatus === 'failed' ? 'Synthetic issue check' : 'Synthetic status task',
2648
2655
  instructionPreview: 'Synthetic fleet scale validation task.',
2649
2656
  status: taskStatus,
2650
- approvalLevel: hasAiAssist ? 'ai-assist' : 'task-only',
2657
+ approvalLevel: 'task-only',
2651
2658
  requestedAt: seenAt,
2652
2659
  sentAt: seenAt,
2653
2660
  updatedAt: seenAt,
2654
- completedAt: taskStatus === 'completed' || taskStatus === 'failed' ? seenAt : '',
2655
- error: taskStatus === 'failed' ? 'Synthetic task failure sample.' : '',
2656
- resultKind: 'synthetic-agent-task',
2657
- resultModel: hasAiAssist ? 'synthetic-ai' : '',
2658
- resultResponseId: hasAiAssist ? `synthetic-response-${ordinal}` : '',
2659
- resultSummary: taskStatus === 'failed'
2660
- ? 'Synthetic task reported a sample issue.'
2661
- : 'Synthetic task completed for scale validation.'
2661
+ completedAt: taskStatus === 'completed' || taskStatus === 'failed' ? seenAt : '',
2662
+ error: taskStatus === 'failed' ? 'Synthetic task failure sample.' : '',
2663
+ resultKind: 'synthetic-agent-task',
2664
+ resultSummary: taskStatus === 'failed'
2665
+ ? 'Synthetic task reported a sample issue.'
2666
+ : 'Synthetic task completed for scale validation.'
2662
2667
  }
2663
2668
  : null;
2664
2669
  const device = {
@@ -2677,11 +2682,8 @@ export function createRemoteHub(options = {}) {
2677
2682
  thumbnail: hasThumbnail,
2678
2683
  control: false,
2679
2684
  liveStream: true,
2680
- computerAgent: true,
2681
- taskDispatch: true,
2682
- aiAssist: hasAiAssist,
2683
- aiModel: hasAiAssist ? 'synthetic-ai' : '',
2684
- aiProvider: hasAiAssist ? 'synthetic' : ''
2685
+ computerAgent: true,
2686
+ taskDispatch: true
2685
2687
  },
2686
2688
  connected: isConnected,
2687
2689
  connectedAt: isConnected ? connectedAt : '',
@@ -2740,31 +2742,12 @@ export function createRemoteHub(options = {}) {
2740
2742
  txMbps: 12 + ((index * 17) % 220),
2741
2743
  usageRatio: Number(Math.min(0.94, ((index * 9) % 81) / 100).toFixed(2))
2742
2744
  },
2743
- workload: {
2744
- role: workloadRole,
2745
- status: taskStatus || (toolBusy ? 'running' : 'idle'),
2746
- title: taskStatus ? 'Fleet validation task' : toolBusy ? 'Processing local job' : 'idle',
2747
- summary: taskStatus ? 'Synthetic status for fleet-scale validation.' : ''
2748
- },
2749
- tools: {
2750
- ollama: {
2751
- status: toolOnline ? (toolBusy ? 'busy' : 'online') : 'offline',
2752
- port: 11434,
2753
- version: toolOnline ? 'local' : ''
2754
- },
2755
- lmStudio: {
2756
- status: index % 5 === 0 ? 'online' : 'offline',
2757
- port: 1234
2758
- },
2759
- comfyui: {
2760
- status: index % 3 === 0 ? (toolBusy ? 'busy' : 'online') : 'offline',
2761
- port: 8188
2762
- },
2763
- stableDiffusion: {
2764
- status: index % 7 === 0 ? 'online' : 'offline',
2765
- port: 7860
2766
- }
2767
- }
2745
+ workload: {
2746
+ role: workloadRole,
2747
+ status: taskStatus || 'idle',
2748
+ title: taskStatus ? 'Fleet validation task' : 'idle',
2749
+ summary: taskStatus ? 'Synthetic status for fleet-scale validation.' : ''
2750
+ }
2768
2751
  },
2769
2752
  latestThumbnail: null,
2770
2753
  latestLiveFrame: null,
@@ -2839,25 +2822,20 @@ export function createRemoteHub(options = {}) {
2839
2822
  if (isConnected) {
2840
2823
  connected += 1;
2841
2824
  }
2842
- if (hasAiAssist) {
2843
- aiAssist += 1;
2844
- }
2845
- devices.set(deviceId, device);
2825
+ devices.set(deviceId, device);
2846
2826
  }
2847
2827
 
2848
2828
  emitRemoteEvent('RemoteSyntheticFleetSeeded', null, {
2849
- count,
2850
- connected,
2851
- aiAssist,
2852
- live
2829
+ count,
2830
+ connected,
2831
+ live
2853
2832
  });
2854
2833
  return {
2855
2834
  ok: true,
2856
2835
  synthetic: true,
2857
- seeded: count,
2858
- connected,
2859
- aiAssist,
2860
- live,
2836
+ seeded: count,
2837
+ connected,
2838
+ live,
2861
2839
  total: devices.size,
2862
2840
  max: MAX_SYNTHETIC_DEVICES
2863
2841
  };
@@ -3754,12 +3732,10 @@ export function createRemoteHub(options = {}) {
3754
3732
  task.updatedAt = now;
3755
3733
  task.completedAt = safeString(result?.completedAt, 80) || now;
3756
3734
  task.error = resultError;
3757
- task.resultSummary = resultError || summarizeTaskResult(result);
3758
- task.resultData = safeTaskData(result?.data);
3759
- task.resultKind = safeString(result?.kind || result?.mode || 'agent-task', 80);
3760
- task.resultModel = safeString(result?.model || result?.aiModel || '', 120);
3761
- task.resultResponseId = safeString(result?.responseId || result?.id || '', 160);
3762
- device.latestTask = task;
3735
+ task.resultSummary = resultError || summarizeTaskResult(result);
3736
+ task.resultData = safeTaskData(result?.data);
3737
+ task.resultKind = safeString(result?.kind || result?.mode || 'agent-task', 80);
3738
+ device.latestTask = task;
3763
3739
  device.pendingTaskCommands.delete(commandId);
3764
3740
  device.counters.taskResultsReceived += 1;
3765
3741
  if (status === 'failed') {
@@ -5862,9 +5838,13 @@ export function createRemoteHub(options = {}) {
5862
5838
  }
5863
5839
 
5864
5840
  function requestAgentTask(deviceId, options = {}) {
5865
- const device = devices.get(String(deviceId || ''));
5866
- const requestedOperation = safeString(options.operation, 80);
5867
- const operation = normalizeAgentOperation(requestedOperation);
5841
+ const retiredRequestError = getRetiredAgentTaskError(options);
5842
+ if (retiredRequestError) {
5843
+ return { ok: false, error: retiredRequestError };
5844
+ }
5845
+ const device = devices.get(String(deviceId || ''));
5846
+ const requestedOperation = safeString(options.operation, 80);
5847
+ const operation = normalizeAgentOperation(requestedOperation);
5868
5848
  if (requestedOperation && !operation) {
5869
5849
  return { ok: false, error: 'unsupported-agent-operation' };
5870
5850
  }
@@ -5882,10 +5862,6 @@ export function createRemoteHub(options = {}) {
5882
5862
 
5883
5863
  const now = new Date().toISOString();
5884
5864
  const approvalLevel = normalizeApprovalLevel(options.approvalLevel);
5885
- if (approvalLevel === 'ai-assist' && !readCapabilityFlag(device.capabilities, 'aiAssist')) {
5886
- return { ok: false, error: 'device-ai-assist-unavailable' };
5887
- }
5888
-
5889
5865
  const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
5890
5866
  const taskId = safeString(options.taskId, 128) || crypto.randomUUID();
5891
5867
  const title = safeString(options.title, 120)
@@ -5907,24 +5883,12 @@ export function createRemoteHub(options = {}) {
5907
5883
  requestedAt: now,
5908
5884
  sentAt: now,
5909
5885
  updatedAt: now,
5910
- completedAt: now,
5911
- error: '',
5912
- resultKind: approvalLevel === 'ai-assist' ? 'synthetic-ai-assist' : 'synthetic-agent-task',
5913
- resultModel: approvalLevel === 'ai-assist'
5914
- ? safeString(options.model, 120) || 'synthetic-ai'
5915
- : '',
5916
- resultResponseId: approvalLevel === 'ai-assist'
5917
- ? `synthetic-response-${taskId}`
5918
- : '',
5919
- resultSummary: operation === 'process.list'
5920
- ? (safeString(options.targetQuery, 160).toLowerCase() === 'comfyui'
5921
- ? (device.status?.tools?.comfyui?.status === 'online'
5922
- ? 'ComfyUI is running.'
5923
- : 'ComfyUI is not running.')
5924
- : 'Process list collected.')
5925
- : approvalLevel === 'ai-assist'
5926
- ? `Synthetic AI assist completed for ${device.deviceName}.`
5927
- : `Synthetic task accepted by ${device.deviceName}.`,
5886
+ completedAt: now,
5887
+ error: '',
5888
+ resultKind: 'synthetic-agent-task',
5889
+ resultSummary: operation === 'process.list'
5890
+ ? 'Process list collected.'
5891
+ : `Synthetic task accepted by ${device.deviceName}.`,
5928
5892
  resultData: undefined
5929
5893
  };
5930
5894
 
@@ -5964,10 +5928,6 @@ export function createRemoteHub(options = {}) {
5964
5928
 
5965
5929
  const now = new Date().toISOString();
5966
5930
  const approvalLevel = normalizeApprovalLevel(options.approvalLevel);
5967
- if (approvalLevel === 'ai-assist' && !readCapabilityFlag(device.capabilities, 'aiAssist')) {
5968
- return { ok: false, error: 'device-ai-assist-unavailable' };
5969
- }
5970
-
5971
5931
  const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
5972
5932
  const taskId = safeString(options.taskId, 128) || crypto.randomUUID();
5973
5933
  const title = safeString(options.title, 120)
@@ -5989,14 +5949,12 @@ export function createRemoteHub(options = {}) {
5989
5949
  requestedAt: now,
5990
5950
  sentAt: now,
5991
5951
  updatedAt: now,
5992
- completedAt: '',
5993
- error: '',
5994
- resultKind: '',
5995
- resultModel: '',
5996
- resultResponseId: '',
5997
- resultSummary: '',
5998
- resultData: undefined
5999
- };
5952
+ completedAt: '',
5953
+ error: '',
5954
+ resultKind: '',
5955
+ resultSummary: '',
5956
+ resultData: undefined
5957
+ };
6000
5958
 
6001
5959
  const commandName = operation || 'agent.task';
6002
5960
  const sent = writeJsonLine(device.socket, {
@@ -6012,8 +5970,7 @@ export function createRemoteHub(options = {}) {
6012
5970
  toolArguments: options.toolArguments && typeof options.toolArguments === 'object' ? options.toolArguments : {},
6013
5971
  permissionMode: safeString(options.permissionMode, 40) || 'ask',
6014
5972
  approvalLevel,
6015
- model: safeString(options.model, 120),
6016
- requestedAt: now
5973
+ requestedAt: now
6017
5974
  },
6018
5975
  issuedAt: now
6019
5976
  });
@@ -6035,15 +5992,26 @@ export function createRemoteHub(options = {}) {
6035
5992
  approvalLevel
6036
5993
  });
6037
5994
  return { ok: true, commandId, taskId, approvalLevel };
6038
- }
6039
-
6040
- function requestAgentTaskBatch(deviceIds, options = {}) {
6041
- const targets = [...new Set((deviceIds || [])
6042
- .map(deviceId => safeString(deviceId, 128))
6043
- .filter(Boolean))]
6044
- .slice(0, 500);
6045
-
6046
- if (targets.length === 0) {
5995
+ }
5996
+
5997
+ function requestAgentTaskBatch(deviceIds, options = {}) {
5998
+ const targets = [...new Set((deviceIds || [])
5999
+ .map(deviceId => safeString(deviceId, 128))
6000
+ .filter(Boolean))]
6001
+ .slice(0, 500);
6002
+ const retiredRequestError = getRetiredAgentTaskError(options);
6003
+ if (retiredRequestError) {
6004
+ return {
6005
+ ok: false,
6006
+ error: retiredRequestError,
6007
+ total: targets.length,
6008
+ queued: 0,
6009
+ approvalLevel: normalizeApprovalLevel(options.approvalLevel),
6010
+ results: []
6011
+ };
6012
+ }
6013
+
6014
+ if (targets.length === 0) {
6047
6015
  return {
6048
6016
  ok: false,
6049
6017
  error: 'no-target-devices',