@livedesk/hub 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "LiveDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -1,7 +1,7 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
- import { AgentSettingsStore, AGENT_PROVIDER_CODEX } from './agent-settings.js';
4
- import { AgentProviderError } from './provider-errors.js';
3
+ import { AgentSettingsStore } from './agent-settings.js';
4
+ import { AgentRuntimeError } from './agent-runtime-error.js';
5
5
 
6
6
  function publicCodexStatus(status = {}) {
7
7
  return {
@@ -13,6 +13,15 @@ function publicCodexStatus(status = {}) {
13
13
  };
14
14
  }
15
15
 
16
+ function publicCodexConnection(result = {}) {
17
+ const latencyMs = Number(result.latencyMs);
18
+ return {
19
+ ok: result.ok === true,
20
+ latencyMs: Number.isFinite(latencyMs) ? Math.max(0, Math.round(latencyMs)) : 0,
21
+ threadId: String(result.threadId || '').slice(0, 200)
22
+ };
23
+ }
24
+
16
25
  export function createAgentManager({
17
26
  dataDir = path.join(os.homedir(), '.livedesk'),
18
27
  settingsStore,
@@ -42,68 +51,21 @@ export function createAgentManager({
42
51
 
43
52
  async function publicSettings() {
44
53
  const current = await settings.get();
45
- const {
46
- codexMaxTurns: _codexMaxTurns,
47
- provider: _provider,
48
- ...exposedSettings
49
- } = current;
50
54
  const codex = await getCodexStatus();
51
- const securityStatus = runtime?.getSecurityStatus?.() || {
52
- isolatedCodexHome: false,
53
- restrictedEnvironment: false,
54
- livedeskMcpOnly: false,
55
- selectedDeviceScopeEnforced: false,
56
- failClosed: true,
57
- verified: false,
58
- state: 'unavailable'
59
- };
60
55
  return {
61
- ...exposedSettings,
56
+ enabled: current.enabled === true,
62
57
  codexInstallation: codex.installed ? 'installed' : 'not-installed',
63
58
  codexAuth: codex.authenticated,
64
- codexStatus: codex.status,
65
- codexPath: codex.codexPath,
66
- agentWorkspacePath: current.codexWorkingDirectory,
67
- securityStatus
59
+ codexStatus: codex.status
68
60
  };
69
61
  }
70
62
 
71
63
  return {
72
64
  getSettings: publicSettings,
73
- async updateSettings(patch = {}) {
74
- const nextPatch = { ...patch };
75
- const requestedProvider = String(nextPatch.provider || AGENT_PROVIDER_CODEX).trim().toLowerCase();
76
- const suppliedLegacyKey = typeof nextPatch.apiKey === 'string' && nextPatch.apiKey.trim().length > 0;
77
- delete nextPatch.provider;
78
- delete nextPatch.apiKey;
79
- if (requestedProvider !== AGENT_PROVIDER_CODEX || suppliedLegacyKey) {
80
- throw new AgentProviderError(
81
- 'agent-provider-retired',
82
- 'LiveDesk uses the Hub-hosted Codex Agent and does not configure alternate model providers.',
83
- { status: 400 }
84
- );
85
- }
86
- await settings.update({ ...nextPatch, provider: AGENT_PROVIDER_CODEX });
87
- statusCache = { expiresAt: 0, value: null };
88
- return publicSettings();
89
- },
90
- async resetSettings() {
91
- await settings.reset();
92
- statusCache = { expiresAt: 0, value: null };
93
- return publicSettings();
94
- },
95
- // Kept as a harmless compatibility endpoint for an older cached web UI.
96
- // No credential is read, written, or returned by the current Hub.
97
- async deleteApiKey() {
98
- return publicSettings();
99
- },
100
65
  async testConnection() {
101
- if (!runtime) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
66
+ if (!runtime) throw new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
102
67
  statusCache = { expiresAt: 0, value: null };
103
- return runtime.testConnection();
104
- },
105
- async createPlan() {
106
- throw new AgentProviderError('agent-codex-run-required', 'Codex Agent requests use the LiveDesk tool loop.', { status: 409 });
68
+ return publicCodexConnection(await runtime.testConnection());
107
69
  },
108
70
  async createSummary(input) {
109
71
  const source = input && typeof input === 'object' ? input : {};
@@ -113,9 +75,8 @@ export function createAgentManager({
113
75
  };
114
76
  },
115
77
  async startRun(input) {
116
- const current = await settings.get();
117
- if (current.provider !== AGENT_PROVIDER_CODEX || !runtime) {
118
- throw new AgentProviderError('agent-codex-not-active', 'Codex SDK is not the active Agent runtime.', { status: 409 });
78
+ if (!runtime) {
79
+ throw new AgentRuntimeError('agent-codex-not-active', 'Codex SDK is not available for LiveDesk Agent commands.', { status: 409 });
119
80
  }
120
81
  return runtime.start(input);
121
82
  },
@@ -0,0 +1,9 @@
1
+ export class AgentRuntimeError extends Error {
2
+ constructor(code, message, { status = 502, retryable = false } = {}) {
3
+ super(message);
4
+ this.name = 'AgentRuntimeError';
5
+ this.code = code;
6
+ this.status = status;
7
+ this.retryable = retryable;
8
+ }
9
+ }
@@ -2,23 +2,13 @@ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
 
5
- export const AGENT_PROVIDER_CODEX = 'codex';
6
- export const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_CODEX;
7
- export const DEFAULT_AGENT_WORKSPACE = path.join(os.homedir(), '.livedesk', 'agent-workspace');
8
5
  export const DEFAULT_AGENT_SETTINGS = Object.freeze({
9
- settingsVersion: 4,
6
+ settingsVersion: 5,
10
7
  enabled: true,
11
- provider: DEFAULT_AGENT_PROVIDER,
12
- codexSandboxMode: 'read-only',
13
- codexApprovalPolicy: 'never',
14
- codexWorkingDirectory: DEFAULT_AGENT_WORKSPACE,
15
8
  codexTaskTimeoutMs: 600000,
16
9
  codexMaxTurns: 12,
17
10
  codexMaxToolCalls: 20,
18
11
  codexResumeSessions: true,
19
- codexShowDetailedEvents: true,
20
- codexNetworkAccessEnabled: false,
21
- codexWebSearchMode: 'disabled',
22
12
  maxConcurrentRequests: 2
23
13
  });
24
14
 
@@ -33,32 +23,15 @@ function numberValue(value, min, max, fallback, integer = false) {
33
23
  return integer ? Math.round(clamped) : clamped;
34
24
  }
35
25
 
36
- function normalizeWorkingDirectory() {
37
- // The agent runtime owns this directory. Do not allow a browser/API caller
38
- // to move Codex into a user project or an arbitrary filesystem root.
39
- return DEFAULT_AGENT_WORKSPACE;
40
- }
41
-
42
26
  export function normalizeAgentSettings(value = {}) {
43
27
  const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
44
28
  return {
45
- settingsVersion: 4,
29
+ settingsVersion: 5,
46
30
  enabled: booleanValue(source.enabled, DEFAULT_AGENT_SETTINGS.enabled),
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,
50
- // These are safety settings, not user-controlled execution toggles. They
51
- // are accepted for compatibility but always normalized to the safe floor.
52
- codexSandboxMode: 'read-only',
53
- codexApprovalPolicy: 'never',
54
- codexWorkingDirectory: normalizeWorkingDirectory(),
55
31
  codexTaskTimeoutMs: numberValue(source.codexTaskTimeoutMs, 30000, 1800000, DEFAULT_AGENT_SETTINGS.codexTaskTimeoutMs, true),
56
32
  codexMaxTurns: numberValue(source.codexMaxTurns, 1, 40, DEFAULT_AGENT_SETTINGS.codexMaxTurns, true),
57
33
  codexMaxToolCalls: numberValue(source.codexMaxToolCalls, 1, 100, DEFAULT_AGENT_SETTINGS.codexMaxToolCalls, true),
58
34
  codexResumeSessions: booleanValue(source.codexResumeSessions, DEFAULT_AGENT_SETTINGS.codexResumeSessions),
59
- codexShowDetailedEvents: booleanValue(source.codexShowDetailedEvents, DEFAULT_AGENT_SETTINGS.codexShowDetailedEvents),
60
- codexNetworkAccessEnabled: false,
61
- codexWebSearchMode: 'disabled',
62
35
  maxConcurrentRequests: numberValue(source.maxConcurrentRequests, 1, 8, DEFAULT_AGENT_SETTINGS.maxConcurrentRequests, true)
63
36
  };
64
37
  }
@@ -95,10 +68,4 @@ export class AgentSettingsStore {
95
68
  return { ...next };
96
69
  }
97
70
 
98
- async reset() {
99
- const next = normalizeAgentSettings(DEFAULT_AGENT_SETTINGS);
100
- await writeJsonAtomic(this.filePath, next);
101
- this.settings = next;
102
- return { ...next };
103
- }
104
71
  }
@@ -4,7 +4,7 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { spawn } from 'node:child_process';
6
6
  import { createRequire } from 'node:module';
7
- import { AgentProviderError } from './provider-errors.js';
7
+ import { AgentRuntimeError } from './agent-runtime-error.js';
8
8
  import { AGENT_TOOL_NAMES } from './agent-tool-registry.js';
9
9
  import { createAgentPermissionPolicy, hashAgentPermissionPolicy } from './agent-permissions.js';
10
10
 
@@ -57,12 +57,12 @@ function isAbortError(error) {
57
57
 
58
58
  function normalizeCodexError(error) {
59
59
  const message = safeText(error?.message, 800) || 'Codex run failed.';
60
- if (error instanceof AgentProviderError && error.code !== 'codex-run-failed') return error;
61
- if (/out of credits|usage limit|rate limit|too many requests/i.test(message)) return new AgentProviderError('codex-usage-limit-reached', 'The Codex workspace has no remaining usage.', { status: 402, retryable: true });
62
- if (/refresh[_ -]?token[_ -]?(?:already[_ -]?)?used|token refresh|refresh credential/i.test(message)) return new AgentProviderError('codex-auth-stale', 'The saved Codex sign-in is stale. LiveDesk refreshed its Codex authentication bridge and will retry.', { status: 401, retryable: true });
63
- if (/not authenticated|not logged in|authentication|unauthorized|login required/i.test(message)) return new AgentProviderError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
64
- if (/user cancelled MCP tool call/i.test(message)) return new AgentProviderError('codex-mcp-tool-rejected', 'Codex rejected the LiveDesk MCP tool before it reached the Hub.', { status: 502 });
65
- return new AgentProviderError('codex-run-failed', message, { status: 502, retryable: true });
60
+ if (error instanceof AgentRuntimeError && error.code !== 'codex-run-failed') return error;
61
+ if (/out of credits|usage limit|rate limit|too many requests/i.test(message)) return new AgentRuntimeError('codex-usage-limit-reached', 'The Codex workspace has no remaining usage.', { status: 402, retryable: true });
62
+ if (/refresh[_ -]?token[_ -]?(?:already[_ -]?)?used|token refresh|refresh credential/i.test(message)) return new AgentRuntimeError('codex-auth-stale', 'The saved Codex sign-in is stale. LiveDesk refreshed its Codex authentication bridge and will retry.', { status: 401, retryable: true });
63
+ if (/not authenticated|not logged in|authentication|unauthorized|login required/i.test(message)) return new AgentRuntimeError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
64
+ if (/user cancelled MCP tool call/i.test(message)) return new AgentRuntimeError('codex-mcp-tool-rejected', 'Codex rejected the LiveDesk MCP tool before it reached the Hub.', { status: 502 });
65
+ return new AgentRuntimeError('codex-run-failed', message, { status: 502, retryable: true });
66
66
  }
67
67
 
68
68
  function codexThreadOptions(workspace) {
@@ -164,16 +164,16 @@ async function ensureCodexHome(codexHome, globalCodexHome, { preferGlobalAuth =
164
164
  const resolvedHome = path.resolve(codexHome);
165
165
  const globalHome = path.resolve(globalCodexHome);
166
166
  if (!resolvedHome || resolvedHome === globalHome) {
167
- throw new AgentProviderError('codex-isolation-unavailable', 'Codex requires a dedicated LiveDesk home.', { status: 503 });
167
+ throw new AgentRuntimeError('codex-isolation-unavailable', 'Codex requires a dedicated LiveDesk home.', { status: 503 });
168
168
  }
169
169
  try {
170
170
  await mkdir(resolvedHome, { recursive: true, mode: 0o700 });
171
171
  if (await access(path.join(resolvedHome, 'config.toml')).then(() => true, () => false)) {
172
- throw new AgentProviderError('codex-environment-isolation-failed', 'The dedicated Codex home contains an unmanaged config.toml.', { status: 503 });
172
+ throw new AgentRuntimeError('codex-environment-isolation-failed', 'The dedicated Codex home contains an unmanaged config.toml.', { status: 503 });
173
173
  }
174
174
  } catch (error) {
175
- if (error instanceof AgentProviderError) throw error;
176
- const wrapped = new AgentProviderError('codex-environment-isolation-failed', 'The dedicated Codex home could not be prepared.', { status: 503 });
175
+ if (error instanceof AgentRuntimeError) throw error;
176
+ const wrapped = new AgentRuntimeError('codex-environment-isolation-failed', 'The dedicated Codex home could not be prepared.', { status: 503 });
177
177
  wrapped.cause = error;
178
178
  throw wrapped;
179
179
  }
@@ -200,7 +200,7 @@ async function ensureCodexHome(codexHome, globalCodexHome, { preferGlobalAuth =
200
200
  await replaceAuthHardLink(sourceAuthPath, targetAuthPath);
201
201
  }
202
202
  } catch (error) {
203
- const wrapped = new AgentProviderError('codex-environment-isolation-failed', 'The Codex authentication bridge could not be prepared.', { status: 503 });
203
+ const wrapped = new AgentRuntimeError('codex-environment-isolation-failed', 'The Codex authentication bridge could not be prepared.', { status: 503 });
204
204
  wrapped.cause = error;
205
205
  throw wrapped;
206
206
  }
@@ -243,7 +243,7 @@ function runAgeMs(run) {
243
243
  }
244
244
 
245
245
  function codexAbortError(code, message) {
246
- return new AgentProviderError(code, message, { status: code === 'codex-run-timeout' ? 504 : 409, retryable: code === 'codex-run-timeout' });
246
+ return new AgentRuntimeError(code, message, { status: code === 'codex-run-timeout' ? 504 : 409, retryable: code === 'codex-run-timeout' });
247
247
  }
248
248
 
249
249
  function safeAgentDeviceIds(value) {
@@ -331,7 +331,7 @@ export function createCodexAgentRuntime({
331
331
  function assertSecurityConfiguration() {
332
332
  const status = securityStatus();
333
333
  if (!status.isolatedCodexHome || !status.restrictedEnvironment || !status.livedeskMcpOnly || !status.selectedDeviceScopeEnforced) {
334
- throw new AgentProviderError('codex-unsupported-security-config', 'Codex security isolation is not available.', { status: 503 });
334
+ throw new AgentRuntimeError('codex-unsupported-security-config', 'Codex security isolation is not available.', { status: 503 });
335
335
  }
336
336
  }
337
337
 
@@ -364,7 +364,7 @@ export function createCodexAgentRuntime({
364
364
 
365
365
  async function loadCodex() {
366
366
  if (!codexModulePromise) codexModulePromise = import('@openai/codex-sdk').catch(error => {
367
- const wrapped = new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed in this LiveDesk package.', { status: 503 });
367
+ const wrapped = new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed in this LiveDesk package.', { status: 503 });
368
368
  wrapped.cause = error;
369
369
  throw wrapped;
370
370
  });
@@ -395,7 +395,7 @@ export function createCodexAgentRuntime({
395
395
  ? 'signed-in'
396
396
  : 'not-signed-in';
397
397
  } catch (error) {
398
- if (error instanceof AgentProviderError) throw error;
398
+ if (error instanceof AgentRuntimeError) throw error;
399
399
  detail = safeText(error?.message, 300);
400
400
  }
401
401
  return { installed: true, authenticated, status: authenticated === 'signed-in' ? 'ready' : 'auth-required', codexPath: pathToCli, detail };
@@ -405,8 +405,8 @@ export function createCodexAgentRuntime({
405
405
  assertSecurityConfiguration();
406
406
  const startedAt = Date.now();
407
407
  const status = await getStatus();
408
- if (!status.installed) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
409
- if (status.authenticated !== 'signed-in') throw new AgentProviderError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
408
+ if (!status.installed) throw new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
409
+ if (status.authenticated !== 'signed-in') throw new AgentRuntimeError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
410
410
  const settings = await settingsStore.get();
411
411
  await mkdir(workspace, { recursive: true });
412
412
  await prepareCodexHome();
@@ -416,7 +416,7 @@ export function createCodexAgentRuntime({
416
416
  try {
417
417
  const thread = codex.startThread(codexThreadOptions(workspace));
418
418
  const turn = await thread.run('Reply with the single word READY. Do not use tools.', { signal: controller.signal });
419
- if (!String(turn.finalResponse || '').trim()) throw new AgentProviderError('codex-empty-response', 'Codex returned an empty response.', { status: 502 });
419
+ if (!String(turn.finalResponse || '').trim()) throw new AgentRuntimeError('codex-empty-response', 'Codex returned an empty response.', { status: 502 });
420
420
  return { ok: true, latencyMs: Date.now() - startedAt, threadId: thread.id || '' };
421
421
  } catch (error) {
422
422
  throw normalizeCodexError(error);
@@ -492,15 +492,15 @@ export function createCodexAgentRuntime({
492
492
  timeoutTimer.unref?.();
493
493
  await mkdir(workspace, { recursive: true });
494
494
  if (activeCount > settings.maxConcurrentRequests) {
495
- throw new AgentProviderError('agent-concurrency-limit', 'The LiveDesk Agent concurrency limit is reached.', { status: 429, retryable: true });
495
+ throw new AgentRuntimeError('agent-concurrency-limit', 'The LiveDesk Agent concurrency limit is reached.', { status: 429, retryable: true });
496
496
  }
497
497
  if (run.abortController.signal.aborted) {
498
498
  if (run.abortReason === 'timeout') throw codexAbortError('codex-run-timeout', 'The Codex task exceeded its timeout.');
499
499
  throw Object.assign(new Error('The Codex task was cancelled.'), { name: 'AbortError' });
500
500
  }
501
501
  const status = await getStatus();
502
- if (!status.installed) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
503
- if (status.authenticated !== 'signed-in') throw new AgentProviderError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
502
+ if (!status.installed) throw new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
503
+ if (status.authenticated !== 'signed-in') throw new AgentRuntimeError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
504
504
  reportProgress(run, 'preparing-tools', 'Codex is ready. Preparing the LiveDesk tools for this Client.');
505
505
  session = createMcpSession({
506
506
  runId: run.runId,
@@ -552,14 +552,14 @@ export function createCodexAgentRuntime({
552
552
  run.abortReason = 'turn-limit';
553
553
  run.abortController.abort();
554
554
  session.cancel();
555
- throw new AgentProviderError('codex-turn-limit-reached', 'Codex exceeded the LiveDesk turn limit.', { status: 409 });
555
+ throw new AgentRuntimeError('codex-turn-limit-reached', 'Codex exceeded the LiveDesk turn limit.', { status: 409 });
556
556
  }
557
557
  }
558
558
  if (run.toolCallCount > run.permissionPolicy.maxToolCalls) {
559
559
  run.abortReason = 'tool-limit';
560
560
  run.abortController.abort();
561
561
  session.cancel();
562
- throw new AgentProviderError('codex-tool-limit-reached', 'Codex exceeded the LiveDesk tool-call limit.', { status: 409 });
562
+ throw new AgentRuntimeError('codex-tool-limit-reached', 'Codex exceeded the LiveDesk tool-call limit.', { status: 409 });
563
563
  }
564
564
  if (event.type === 'item.completed' && event.item?.type === 'mcp_tool_call' && event.item?.status === 'failed') {
565
565
  throw new Error(safeText(event.item?.error?.message, 800) || `LiveDesk tool ${safeText(event.item?.tool, 120) || 'call'} failed.`);
@@ -641,14 +641,14 @@ export function createCodexAgentRuntime({
641
641
 
642
642
  async function start(input = {}) {
643
643
  const instruction = safeText(input.instruction, 4000);
644
- if (!instruction) throw new AgentProviderError('agent-invalid-instruction', 'Instruction is required.', { status: 400 });
644
+ if (!instruction) throw new AgentRuntimeError('agent-invalid-instruction', 'Instruction is required.', { status: 400 });
645
645
  const deviceIds = safeAgentDeviceIds(input.deviceIds);
646
- if (deviceIds.length === 0) throw new AgentProviderError('agent-no-target-devices', 'Select at least one connected device.', { status: 400 });
646
+ if (deviceIds.length === 0) throw new AgentRuntimeError('agent-no-target-devices', 'Select at least one connected device.', { status: 400 });
647
647
  const settings = await settingsStore.get();
648
- if (!settings.enabled) throw new AgentProviderError('agent-ai-disabled', 'Codex Agent is disabled in Settings.', { status: 409 });
648
+ if (!settings.enabled) throw new AgentRuntimeError('agent-disabled', 'Codex Agent is disabled in Settings.', { status: 409 });
649
649
  assertSecurityConfiguration();
650
650
  pruneRuns();
651
- if (runs.size >= MAX_RUNS) throw new AgentProviderError('agent-run-limit', 'The LiveDesk Agent run history is full.', { status: 429, retryable: true });
651
+ if (runs.size >= MAX_RUNS) throw new AgentRuntimeError('agent-run-limit', 'The LiveDesk Agent run history is full.', { status: 429, retryable: true });
652
652
  const permissionPolicy = freezeAgentPermissionPolicy(input.permissionPolicy && typeof input.permissionPolicy === 'object'
653
653
  ? input.permissionPolicy
654
654
  : createAgentPermissionPolicy({ mode: input.permissionMode || 'safe-auto', deviceIds, maxToolCalls: settings.codexMaxToolCalls }));
package/src/remote-hub.js CHANGED
@@ -3892,20 +3892,34 @@ export function createRemoteHub(options = {}) {
3892
3892
  agentQueueBeforePaceMs: Number.isFinite(Number(message.agentQueueBeforePaceMs)) ? Number(message.agentQueueBeforePaceMs) : 0,
3893
3893
  agentFrameAgeMs: Number.isFinite(Number(message.agentFrameAgeMs)) ? Number(message.agentFrameAgeMs) : 0,
3894
3894
  nativeCaptureTelemetryAvailable: message.nativeCaptureTelemetryAvailable === true,
3895
+ nativeCaptureTelemetryVersion: Number.isFinite(Number(message.nativeCaptureTelemetryVersion)) ? Number(message.nativeCaptureTelemetryVersion) : 0,
3896
+ nativeCaptureTelemetryAgeMs: message.nativeCaptureTelemetryAgeMs === null
3897
+ || message.nativeCaptureTelemetryAgeMs === undefined
3898
+ ? undefined
3899
+ : Number.isFinite(Number(message.nativeCaptureTelemetryAgeMs))
3900
+ ? Math.max(0, Number(message.nativeCaptureTelemetryAgeMs))
3901
+ : undefined,
3895
3902
  nativeEncoderHardwareAccelerationKnown: message.nativeEncoderHardwareAccelerationKnown === true,
3896
3903
  nativeEncoderHardwareAccelerated: typeof message.nativeEncoderHardwareAccelerated === 'boolean'
3897
3904
  ? message.nativeEncoderHardwareAccelerated
3898
3905
  : null,
3899
3906
  nativeEncoderHardwareQueryStatus: safeString(message.nativeEncoderHardwareQueryStatus, 80),
3900
3907
  nativeCaptureFps: Number.isFinite(Number(message.nativeCaptureFps)) ? Number(message.nativeCaptureFps) : 0,
3908
+ nativeCompressionFps: Number.isFinite(Number(message.nativeCompressionFps)) ? Number(message.nativeCompressionFps) : 0,
3901
3909
  nativeEncodedFps: Number.isFinite(Number(message.nativeEncodedFps)) ? Number(message.nativeEncodedFps) : 0,
3902
3910
  nativeCaptureSampleCount: Number.isFinite(Number(message.nativeCaptureSampleCount)) ? Number(message.nativeCaptureSampleCount) : 0,
3903
3911
  nativeEncodableFrameCount: Number.isFinite(Number(message.nativeEncodableFrameCount)) ? Number(message.nativeEncodableFrameCount) : 0,
3904
3912
  nativeSkippedFrameCount: Number.isFinite(Number(message.nativeSkippedFrameCount)) ? Number(message.nativeSkippedFrameCount) : 0,
3913
+ nativeCompressionOutputCount: Number.isFinite(Number(message.nativeCompressionOutputCount)) ? Number(message.nativeCompressionOutputCount) : 0,
3905
3914
  nativeEncodedFrameCount: Number.isFinite(Number(message.nativeEncodedFrameCount)) ? Number(message.nativeEncodedFrameCount) : 0,
3906
3915
  nativeEncodeFramesInFlight: Number.isFinite(Number(message.nativeEncodeFramesInFlight)) ? Number(message.nativeEncodeFramesInFlight) : 0,
3907
3916
  nativeBackpressureSkippedFrameCount: Number.isFinite(Number(message.nativeBackpressureSkippedFrameCount)) ? Number(message.nativeBackpressureSkippedFrameCount) : 0,
3908
3917
  nativeIdleSkippedFrameCount: Number.isFinite(Number(message.nativeIdleSkippedFrameCount)) ? Number(message.nativeIdleSkippedFrameCount) : 0,
3918
+ nativeOutputQueueDepth: Number.isFinite(Number(message.nativeOutputQueueDepth)) ? Number(message.nativeOutputQueueDepth) : 0,
3919
+ nativeOutputQueueBytes: Number.isFinite(Number(message.nativeOutputQueueBytes)) ? Number(message.nativeOutputQueueBytes) : 0,
3920
+ nativeOutputQueueHighWatermark: Number.isFinite(Number(message.nativeOutputQueueHighWatermark)) ? Number(message.nativeOutputQueueHighWatermark) : 0,
3921
+ nativeOutputQueueDroppedCount: Number.isFinite(Number(message.nativeOutputQueueDroppedCount)) ? Number(message.nativeOutputQueueDroppedCount) : 0,
3922
+ nativeOutputQueueAwaitingKeyFrame: message.nativeOutputQueueAwaitingKeyFrame === true,
3909
3923
  nativeCaptureTelemetryAt: safeString(message.nativeCaptureTelemetryAt, 80),
3910
3924
  droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
3911
3925
  hardwareEncoder: safeString(message.hardwareEncoder, 80),
@@ -4274,20 +4288,34 @@ export function createRemoteHub(options = {}) {
4274
4288
  agentQueueBeforePaceMs: Number.isFinite(Number(message.agentQueueBeforePaceMs)) ? Number(message.agentQueueBeforePaceMs) : 0,
4275
4289
  agentFrameAgeMs: Number.isFinite(Number(message.agentFrameAgeMs)) ? Number(message.agentFrameAgeMs) : 0,
4276
4290
  nativeCaptureTelemetryAvailable: message.nativeCaptureTelemetryAvailable === true,
4291
+ nativeCaptureTelemetryVersion: Number.isFinite(Number(message.nativeCaptureTelemetryVersion)) ? Number(message.nativeCaptureTelemetryVersion) : 0,
4292
+ nativeCaptureTelemetryAgeMs: message.nativeCaptureTelemetryAgeMs === null
4293
+ || message.nativeCaptureTelemetryAgeMs === undefined
4294
+ ? undefined
4295
+ : Number.isFinite(Number(message.nativeCaptureTelemetryAgeMs))
4296
+ ? Math.max(0, Number(message.nativeCaptureTelemetryAgeMs))
4297
+ : undefined,
4277
4298
  nativeEncoderHardwareAccelerationKnown: message.nativeEncoderHardwareAccelerationKnown === true,
4278
4299
  nativeEncoderHardwareAccelerated: typeof message.nativeEncoderHardwareAccelerated === 'boolean'
4279
4300
  ? message.nativeEncoderHardwareAccelerated
4280
4301
  : null,
4281
4302
  nativeEncoderHardwareQueryStatus: safeString(message.nativeEncoderHardwareQueryStatus, 80),
4282
4303
  nativeCaptureFps: Number.isFinite(Number(message.nativeCaptureFps)) ? Number(message.nativeCaptureFps) : 0,
4304
+ nativeCompressionFps: Number.isFinite(Number(message.nativeCompressionFps)) ? Number(message.nativeCompressionFps) : 0,
4283
4305
  nativeEncodedFps: Number.isFinite(Number(message.nativeEncodedFps)) ? Number(message.nativeEncodedFps) : 0,
4284
4306
  nativeCaptureSampleCount: Number.isFinite(Number(message.nativeCaptureSampleCount)) ? Number(message.nativeCaptureSampleCount) : 0,
4285
4307
  nativeEncodableFrameCount: Number.isFinite(Number(message.nativeEncodableFrameCount)) ? Number(message.nativeEncodableFrameCount) : 0,
4286
4308
  nativeSkippedFrameCount: Number.isFinite(Number(message.nativeSkippedFrameCount)) ? Number(message.nativeSkippedFrameCount) : 0,
4309
+ nativeCompressionOutputCount: Number.isFinite(Number(message.nativeCompressionOutputCount)) ? Number(message.nativeCompressionOutputCount) : 0,
4287
4310
  nativeEncodedFrameCount: Number.isFinite(Number(message.nativeEncodedFrameCount)) ? Number(message.nativeEncodedFrameCount) : 0,
4288
4311
  nativeEncodeFramesInFlight: Number.isFinite(Number(message.nativeEncodeFramesInFlight)) ? Number(message.nativeEncodeFramesInFlight) : 0,
4289
4312
  nativeBackpressureSkippedFrameCount: Number.isFinite(Number(message.nativeBackpressureSkippedFrameCount)) ? Number(message.nativeBackpressureSkippedFrameCount) : 0,
4290
4313
  nativeIdleSkippedFrameCount: Number.isFinite(Number(message.nativeIdleSkippedFrameCount)) ? Number(message.nativeIdleSkippedFrameCount) : 0,
4314
+ nativeOutputQueueDepth: Number.isFinite(Number(message.nativeOutputQueueDepth)) ? Number(message.nativeOutputQueueDepth) : 0,
4315
+ nativeOutputQueueBytes: Number.isFinite(Number(message.nativeOutputQueueBytes)) ? Number(message.nativeOutputQueueBytes) : 0,
4316
+ nativeOutputQueueHighWatermark: Number.isFinite(Number(message.nativeOutputQueueHighWatermark)) ? Number(message.nativeOutputQueueHighWatermark) : 0,
4317
+ nativeOutputQueueDroppedCount: Number.isFinite(Number(message.nativeOutputQueueDroppedCount)) ? Number(message.nativeOutputQueueDroppedCount) : 0,
4318
+ nativeOutputQueueAwaitingKeyFrame: message.nativeOutputQueueAwaitingKeyFrame === true,
4291
4319
  nativeCaptureTelemetryAt: safeString(message.nativeCaptureTelemetryAt, 80),
4292
4320
  droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
4293
4321
  hubIngressDropped: Number.isFinite(Number(message.hubIngressDropped)) ? Number(message.hubIngressDropped) : 0,
package/src/server.js CHANGED
@@ -19,7 +19,7 @@ import { createAgentManager } from './agents/agent-manager.js';
19
19
  import { AgentSettingsStore } from './agents/agent-settings.js';
20
20
  import { createCodexAgentRuntime } from './agents/codex-agent-runtime.js';
21
21
  import { createAgentDeviceScope, resolveAgentTargetIds } from './agents/agent-device-scope.js';
22
- import { AgentProviderError } from './agents/provider-errors.js';
22
+ import { AgentRuntimeError } from './agents/agent-runtime-error.js';
23
23
  import { AGENT_PERMISSION_MODES, createAgentPermissionPolicy, evaluateAgentToolPermission, hashAgentPermissionPolicy } from './agents/agent-permissions.js';
24
24
  import { createAgentPermissionStore } from './agents/agent-permission-store.js';
25
25
  import { createAgentAuditStore } from './agents/agent-audit-store.js';
@@ -931,7 +931,7 @@ function createAgentMcpSession({ runId, signal, allowedDeviceIds: allowedDeviceI
931
931
  const allowedDeviceIds = createAgentDeviceScope(allowedDeviceIdsInput);
932
932
  const initialResolution = resolveAgentTargetIds({ allowedDeviceIds, connectedDeviceIds: connectedAgentDeviceIds() });
933
933
  if (initialResolution.deviceIds.length === 0) {
934
- throw new AgentProviderError(initialResolution.error || 'agent-no-target-devices', 'No selected connected device is available.', { status: 400 });
934
+ throw new AgentRuntimeError(initialResolution.error || 'agent-no-target-devices', 'No selected connected device is available.', { status: 400 });
935
935
  }
936
936
  pruneAgentMcpRunBatches();
937
937
  const effectivePermissionPolicy = permissionPolicy || createAgentPermissionPolicy({ mode: 'ask', deviceIds: allowedDeviceIdsInput, maxToolCalls });
@@ -1376,7 +1376,7 @@ function sendAgentError(res, error) {
1376
1376
  const code = String(error?.code || 'agent-request-failed').replace(/[^a-z0-9-]/gi, '-').toLowerCase().slice(0, 80);
1377
1377
  const status = Number.isInteger(error?.status) && error.status >= 400 && error.status <= 599
1378
1378
  ? error.status
1379
- : code === 'agent-api-key-missing' || code === 'agent-api-key-invalid' || code === 'codex-auth-required' ? 401
1379
+ : code === 'codex-auth-required' ? 401
1380
1380
  : code === 'agent-no-target-devices' || code === 'agent-target-outside-selection' || code === 'codex-tool-not-allowed' || code === 'agent-permission-denied' || code === 'agent-approval-required' || code === 'agent-approval-rejected' || code === 'agent-approval-expired' || code === 'agent-tool-single-target-required' || code === 'agent-tool-platform-unsupported' || code === 'agent-tool-capability-unavailable' ? 400
1381
1381
  : code === 'agent-mcp-loopback-only' ? 403
1382
1382
  : code === 'codex-isolation-unavailable' || code === 'codex-environment-isolation-failed' || code === 'codex-unsupported-security-config' ? 503
@@ -2102,20 +2102,34 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
2102
2102
  agentQueueBeforePaceMs: Number(frame.agentQueueBeforePaceMs || 0) || 0,
2103
2103
  agentFrameAgeMs: Number(frame.agentFrameAgeMs || 0) || 0,
2104
2104
  nativeCaptureTelemetryAvailable: frame.nativeCaptureTelemetryAvailable === true,
2105
+ nativeCaptureTelemetryVersion: Number(frame.nativeCaptureTelemetryVersion || 0) || 0,
2106
+ nativeCaptureTelemetryAgeMs: frame.nativeCaptureTelemetryAgeMs === null
2107
+ || frame.nativeCaptureTelemetryAgeMs === undefined
2108
+ ? undefined
2109
+ : Number.isFinite(Number(frame.nativeCaptureTelemetryAgeMs))
2110
+ ? Math.max(0, Number(frame.nativeCaptureTelemetryAgeMs))
2111
+ : undefined,
2105
2112
  nativeEncoderHardwareAccelerationKnown: frame.nativeEncoderHardwareAccelerationKnown === true,
2106
2113
  nativeEncoderHardwareAccelerated: typeof frame.nativeEncoderHardwareAccelerated === 'boolean'
2107
2114
  ? frame.nativeEncoderHardwareAccelerated
2108
2115
  : null,
2109
2116
  nativeEncoderHardwareQueryStatus: frame.nativeEncoderHardwareQueryStatus || '',
2110
2117
  nativeCaptureFps: Number(frame.nativeCaptureFps || 0) || 0,
2118
+ nativeCompressionFps: Number(frame.nativeCompressionFps || 0) || 0,
2111
2119
  nativeEncodedFps: Number(frame.nativeEncodedFps || 0) || 0,
2112
2120
  nativeCaptureSampleCount: Number(frame.nativeCaptureSampleCount || 0) || 0,
2113
2121
  nativeEncodableFrameCount: Number(frame.nativeEncodableFrameCount || 0) || 0,
2114
2122
  nativeSkippedFrameCount: Number(frame.nativeSkippedFrameCount || 0) || 0,
2123
+ nativeCompressionOutputCount: Number(frame.nativeCompressionOutputCount || 0) || 0,
2115
2124
  nativeEncodedFrameCount: Number(frame.nativeEncodedFrameCount || 0) || 0,
2116
2125
  nativeEncodeFramesInFlight: Number(frame.nativeEncodeFramesInFlight || 0) || 0,
2117
2126
  nativeBackpressureSkippedFrameCount: Number(frame.nativeBackpressureSkippedFrameCount || 0) || 0,
2118
2127
  nativeIdleSkippedFrameCount: Number(frame.nativeIdleSkippedFrameCount || 0) || 0,
2128
+ nativeOutputQueueDepth: Number(frame.nativeOutputQueueDepth || 0) || 0,
2129
+ nativeOutputQueueBytes: Number(frame.nativeOutputQueueBytes || 0) || 0,
2130
+ nativeOutputQueueHighWatermark: Number(frame.nativeOutputQueueHighWatermark || 0) || 0,
2131
+ nativeOutputQueueDroppedCount: Number(frame.nativeOutputQueueDroppedCount || 0) || 0,
2132
+ nativeOutputQueueAwaitingKeyFrame: frame.nativeOutputQueueAwaitingKeyFrame === true,
2119
2133
  nativeCaptureTelemetryAt: frame.nativeCaptureTelemetryAt || '',
2120
2134
  droppedByAgent: Number(frame.droppedByAgent || 0) || 0,
2121
2135
  hubIngressDropped: Number(frame.hubIngressDropped || 0) || 0,
@@ -2925,48 +2939,7 @@ app.get('/api/settings/agent/audit', async (req, res) => {
2925
2939
  res.json({ ok: true, audit: await agentAuditStore.list({ runId: req.query?.runId || '', limit: req.query?.limit }) });
2926
2940
  });
2927
2941
 
2928
- app.put('/api/settings/agent', async (req, res) => {
2929
- noStore(res);
2930
- try {
2931
- const patch = req.body && typeof req.body === 'object' && !Array.isArray(req.body) ? { ...req.body } : {};
2932
- delete patch.enabled;
2933
- patch.enabled = await synchronizeAgentEnablement();
2934
- res.json({ ok: true, settings: await agentManager.updateSettings(patch) });
2935
- } catch (error) {
2936
- sendAgentError(res, error);
2937
- }
2938
- });
2939
-
2940
- app.post('/api/settings/agent/reset', async (_req, res) => {
2941
- noStore(res);
2942
- try {
2943
- await agentManager.resetSettings();
2944
- await synchronizeAgentEnablement();
2945
- res.json({ ok: true, settings: await agentManager.getSettings() });
2946
- } catch (error) {
2947
- sendAgentError(res, error);
2948
- }
2949
- });
2950
-
2951
- app.delete('/api/settings/agent/api-key', async (_req, res) => {
2952
- noStore(res);
2953
- try {
2954
- res.json({ ok: true, settings: await agentManager.deleteApiKey() });
2955
- } catch (error) {
2956
- sendAgentError(res, error);
2957
- }
2958
- });
2959
-
2960
- app.get('/api/settings/agent/models', async (_req, res) => {
2961
- noStore(res);
2962
- res.status(410).json({
2963
- ok: false,
2964
- error: 'agent-model-catalog-retired',
2965
- message: 'LiveDesk uses the Codex SDK defaults and does not manage models.'
2966
- });
2967
- });
2968
-
2969
- app.post('/api/settings/agent/test', async (_req, res) => {
2942
+ app.post('/api/settings/agent/test', async (_req, res) => {
2970
2943
  noStore(res);
2971
2944
  try {
2972
2945
  res.json({ ok: true, connection: await agentManager.testConnection() });
@@ -3001,17 +2974,17 @@ app.post('/api/settings/agent/run', async (req, res) => {
3001
2974
  noStore(res);
3002
2975
  try {
3003
2976
  if (!(await synchronizeAgentEnablement())) {
3004
- throw new AgentProviderError('agent-ai-disabled', 'Enable Codex Agent in Settings before running commands.', { status: 409 });
2977
+ throw new AgentRuntimeError('agent-disabled', 'Enable Codex Agent in Settings before running commands.', { status: 409 });
3005
2978
  }
3006
2979
  const instruction = typeof req.body?.instruction === 'string' ? req.body.instruction.slice(0, 4000) : '';
3007
2980
  const deviceIds = normalizeDeviceIds(req.body?.deviceIds).slice(0, 500);
3008
2981
  const connectedDeviceIds = new Set(connectedAgentDeviceIds());
3009
2982
  if (deviceIds.length === 0 || deviceIds.some(deviceId => !connectedDeviceIds.has(deviceId))) {
3010
- throw new AgentProviderError('agent-target-not-connected', 'Every selected Client must have an active Agent control channel.', { status: 409 });
2983
+ throw new AgentRuntimeError('agent-target-not-connected', 'Every selected Client must have an active Agent control channel.', { status: 409 });
3011
2984
  }
3012
2985
  const permissionMode = String(req.body?.permissionMode || 'ask').trim().toLowerCase();
3013
2986
  if (!AGENT_PERMISSION_MODES.includes(permissionMode)) {
3014
- throw new AgentProviderError('agent-policy-invalid', 'Select a valid Agent permission mode.', { status: 400 });
2987
+ throw new AgentRuntimeError('agent-policy-invalid', 'Select a valid Agent permission mode.', { status: 400 });
3015
2988
  }
3016
2989
  const settings = await agentSettingsStore.get();
3017
2990
  const customPolicy = permissionMode === 'custom' ? await agentPermissionStore.get(req.body?.customPolicyId || 'custom-default') : null;
@@ -3109,17 +3082,7 @@ app.post('/api/settings/agent/runs/cancel-all', async (_req, res) => {
3109
3082
  res.json({ ok: true, cancelledRuns, cancelledSessions, cancelledBatches: cancelledBatchIds.size });
3110
3083
  });
3111
3084
 
3112
- app.post('/api/settings/agent/plan', async (req, res) => {
3113
- noStore(res);
3114
- try {
3115
- const instruction = typeof req.body?.instruction === 'string' ? req.body.instruction.slice(0, 2000) : '';
3116
- res.json({ ok: true, plan: await agentManager.createPlan({ instruction }) });
3117
- } catch (error) {
3118
- sendAgentError(res, error);
3119
- }
3120
- });
3121
-
3122
- app.post('/api/settings/agent/summary', async (req, res) => {
3085
+ app.post('/api/settings/agent/summary', async (req, res) => {
3123
3086
  noStore(res);
3124
3087
  try {
3125
3088
  res.json({ ok: true, ...await agentManager.createSummary(req.body || {}) });
@@ -1,12 +0,0 @@
1
- export class AgentProviderError extends Error {
2
- constructor(code, message, { status = 502, retryable = false } = {}) {
3
- super(message);
4
- this.name = 'AgentProviderError';
5
- this.code = code;
6
- this.status = status;
7
- this.retryable = retryable;
8
- }
9
- }
10
- export function isRetryableProviderError(error) {
11
- return error?.retryable === true || ['agent-provider-timeout', 'agent-provider-network', 'agent-provider-rate-limited', 'agent-provider-upstream'].includes(error?.code);
12
- }