livedesk 0.1.451 → 0.1.453

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.
@@ -47,8 +47,7 @@ const FRESH_REGISTRY_TIMEOUT_MAX_MS = 10000;
47
47
  // polling must stay interactive instead of backing off to multi-minute waits.
48
48
  const DISCOVERY_RETRY_SCHEDULE_MS = [750, 750, 750, 750, 750, 750, 750, 750, 5000];
49
49
  const DISCOVERY_RETRY_JITTER_RATIO = 0.2;
50
- const HUB_WAKE_QUERY_JITTER_MAX_MS = 2000;
51
- const EXIT_INVALID_PAIR_TOKEN = 23;
50
+ const EXIT_INVALID_PAIR_TOKEN = 23;
52
51
  const EXIT_CLIENT_UPDATE = 42;
53
52
  const SESSION_REFRESH_SKEW_SECONDS = 60;
54
53
  const HUB_TARGET_CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
@@ -2706,9 +2705,10 @@ export async function resolveManagerFromPin(supabase, pin, options = {}) {
2706
2705
  allowRelayFallback: options.allowRelayFallback === true,
2707
2706
  relayEndpoint: options.relayEndpoint,
2708
2707
  probeEndpoint: options.probeEndpoint,
2709
- resolveFreshTarget: async () => {
2708
+ resolveFreshTarget: async signal => {
2710
2709
  const { data, error } = await supabase.functions.invoke('livedesk-resolve-remote-host', {
2711
- body: { pin: normalizedPin }
2710
+ body: { pin: normalizedPin },
2711
+ signal
2712
2712
  });
2713
2713
  if (error) {
2714
2714
  throw await normalizePinResolverInvocationError(error);
@@ -3667,28 +3667,73 @@ export function resolveManagerFromCachedRelayFallback(ownerKey, options = {}) {
3667
3667
  };
3668
3668
  }
3669
3669
 
3670
+ function normalizeFreshRegistryTimeoutMs(value) {
3671
+ const configured = Number(value);
3672
+ return Number.isFinite(configured)
3673
+ ? Math.max(FRESH_REGISTRY_TIMEOUT_MIN_MS, Math.min(FRESH_REGISTRY_TIMEOUT_MAX_MS, configured))
3674
+ : FRESH_REGISTRY_TIMEOUT_MS;
3675
+ }
3676
+
3677
+ export async function runFreshHubRegistryLookup(resolveFreshTarget, options = {}) {
3678
+ if (typeof resolveFreshTarget !== 'function') {
3679
+ throw createHubDiscoveryError(
3680
+ 'fresh-registry-resolver-required',
3681
+ 'Fresh Hub registry resolver is required.'
3682
+ );
3683
+ }
3684
+ const timeoutMs = normalizeFreshRegistryTimeoutMs(options.timeoutMs);
3685
+ const controller = new AbortController();
3686
+ let timeoutHandle = null;
3687
+ const lookupOutcome = Promise.resolve()
3688
+ .then(() => resolveFreshTarget(controller.signal))
3689
+ .then(
3690
+ target => ({ type: 'target', target }),
3691
+ error => ({ type: 'error', error })
3692
+ );
3693
+ const timeoutOutcome = new Promise(resolveTimeout => {
3694
+ timeoutHandle = setTimeout(
3695
+ () => resolveTimeout({ type: 'timeout' }),
3696
+ timeoutMs
3697
+ );
3698
+ });
3699
+ const outcomes = [lookupOutcome, timeoutOutcome];
3700
+ if (options.wakePromise && typeof options.wakePromise.then === 'function') {
3701
+ outcomes.push(Promise.resolve(options.wakePromise).then(
3702
+ event => ({ type: 'hub-online', event }),
3703
+ () => new Promise(() => {})
3704
+ ));
3705
+ }
3706
+ const outcome = await Promise.race(outcomes).finally(() => {
3707
+ clearTimeout(timeoutHandle);
3708
+ });
3709
+ if (outcome.type === 'hub-online') {
3710
+ controller.abort('hub-online');
3711
+ return outcome;
3712
+ }
3713
+ if (outcome.type === 'timeout') {
3714
+ controller.abort('hub-registry-timeout');
3715
+ throw createHubDiscoveryError(
3716
+ 'hub-registry-timeout',
3717
+ `Fresh LiveDesk Hub registry lookup exceeded ${timeoutMs}ms.`
3718
+ );
3719
+ }
3720
+ if (outcome.type === 'error') {
3721
+ controller.abort('hub-registry-error');
3722
+ throw outcome.error;
3723
+ }
3724
+ return outcome;
3725
+ }
3726
+
3670
3727
  export async function resolveManagerWithCachedRecovery(ownerKey, options = {}) {
3671
3728
  const cached = Object.prototype.hasOwnProperty.call(options, 'cachedTarget')
3672
3729
  ? normalizeCachedHubTarget(options.cachedTarget, ownerKey, options.now)
3673
3730
  : readCachedHubTarget(ownerKey);
3674
3731
  try {
3675
- if (typeof options.resolveFreshTarget !== 'function') {
3676
- throw createHubDiscoveryError('fresh-registry-resolver-required', 'Fresh Hub registry resolver is required.');
3677
- }
3678
- const configuredFreshTimeoutMs = Number(options.freshTimeoutMs);
3679
- const freshTimeoutMs = Number.isFinite(configuredFreshTimeoutMs)
3680
- ? Math.max(FRESH_REGISTRY_TIMEOUT_MIN_MS, Math.min(FRESH_REGISTRY_TIMEOUT_MAX_MS, configuredFreshTimeoutMs))
3681
- : FRESH_REGISTRY_TIMEOUT_MS;
3682
- let timeoutHandle = null;
3683
- const fresh = await Promise.race([
3684
- Promise.resolve().then(() => options.resolveFreshTarget()),
3685
- new Promise((_, reject) => {
3686
- timeoutHandle = setTimeout(() => reject(createHubDiscoveryError(
3687
- 'hub-registry-timeout',
3688
- `Fresh LiveDesk Hub registry lookup exceeded ${freshTimeoutMs}ms.`
3689
- )), freshTimeoutMs);
3690
- })
3691
- ]).finally(() => clearTimeout(timeoutHandle));
3732
+ const freshOutcome = await runFreshHubRegistryLookup(
3733
+ options.resolveFreshTarget,
3734
+ { timeoutMs: options.freshTimeoutMs }
3735
+ );
3736
+ const fresh = freshOutcome.target;
3692
3737
  if (!fresh?.manager || !fresh?.pair) {
3693
3738
  throw createHubDiscoveryError('fresh-registry-target-invalid', 'Fresh Hub registry target is incomplete.');
3694
3739
  }
@@ -3770,12 +3815,21 @@ function normalizeEndpointCandidates(target) {
3770
3815
  }
3771
3816
 
3772
3817
  export async function resolveManagerFromSupabase(supabase, options = {}) {
3773
- await refreshSessionIfNeeded(supabase);
3774
- const { data, error } = await supabase
3775
- .from('livedesk_remote_host_targets')
3776
- .select('node_id, endpoint, endpoint_candidates, pair_token, active, expires_at, updated_at, manager_version')
3777
- .eq('product_key', 'livedesk')
3778
- .maybeSingle();
3818
+ if (options.signal?.aborted) {
3819
+ throw createHubDiscoveryError('hub-registry-aborted', 'LiveDesk Hub registry lookup was cancelled.');
3820
+ }
3821
+ await refreshSessionIfNeeded(supabase);
3822
+ if (options.signal?.aborted) {
3823
+ throw createHubDiscoveryError('hub-registry-aborted', 'LiveDesk Hub registry lookup was cancelled.');
3824
+ }
3825
+ let query = supabase
3826
+ .from('livedesk_remote_host_targets')
3827
+ .select('node_id, endpoint, endpoint_candidates, pair_token, active, expires_at, updated_at, manager_version')
3828
+ .eq('product_key', 'livedesk');
3829
+ if (options.signal && typeof query.abortSignal === 'function') {
3830
+ query = query.abortSignal(options.signal);
3831
+ }
3832
+ const { data, error } = await query.maybeSingle();
3779
3833
  if (error) {
3780
3834
  throw error;
3781
3835
  }
@@ -3849,9 +3903,12 @@ async function registerClientDeviceWithSupabase(supabase, options = {}) {
3849
3903
  }
3850
3904
  }
3851
3905
 
3852
- async function waitForManagerFromSupabase(supabase, options = {}) {
3906
+ export async function waitForManagerFromSupabase(supabase, options = {}) {
3853
3907
  const configuredIntervalMs = Number(options.intervalMs || 0);
3854
3908
  const shouldStop = typeof options.shouldStop === 'function' ? options.shouldStop : () => false;
3909
+ const createWakeListener = typeof options.createWakeListener === 'function'
3910
+ ? options.createWakeListener
3911
+ : createHubWakeListener;
3855
3912
  let attempts = 0;
3856
3913
  let lastMessage = '';
3857
3914
  let wakeListener = null;
@@ -3862,6 +3919,17 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
3862
3919
  if (shouldStop()) {
3863
3920
  return null;
3864
3921
  }
3922
+ if (!wakeListener) {
3923
+ try {
3924
+ wakeListener = await createWakeListener({
3925
+ getAccessToken: async () => (await refreshSessionIfNeeded(supabase))?.access_token || ''
3926
+ });
3927
+ } catch (error) {
3928
+ if (attempts === 0) {
3929
+ console.warn(`[LiveDesk Client] Hub-online event channel unavailable; adaptive registry retries remain active: ${error?.message || error}`);
3930
+ }
3931
+ }
3932
+ }
3865
3933
  attempts += 1;
3866
3934
  try {
3867
3935
  if (initialError) {
@@ -3869,9 +3937,24 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
3869
3937
  initialError = null;
3870
3938
  throw error;
3871
3939
  }
3872
- return await resolveManagerFromSupabase(supabase, {
3873
- allowRelayFallback: options.allowRelayFallback === true
3874
- });
3940
+ const outcome = await runFreshHubRegistryLookup(
3941
+ signal => resolveManagerFromSupabase(supabase, {
3942
+ allowRelayFallback: options.allowRelayFallback === true,
3943
+ probeEndpoint: options.probeEndpoint,
3944
+ signal
3945
+ }),
3946
+ {
3947
+ timeoutMs: options.freshTimeoutMs,
3948
+ wakePromise: wakeListener?.promise
3949
+ }
3950
+ );
3951
+ if (outcome.type === 'hub-online') {
3952
+ wakeListener?.close();
3953
+ wakeListener = null;
3954
+ console.log('[LiveDesk Client] Hub-online event received. Rechecking the registry now.');
3955
+ continue;
3956
+ }
3957
+ return outcome.target;
3875
3958
  } catch (err) {
3876
3959
  const message = formatDiscoveryError(err);
3877
3960
  if (message !== lastMessage || attempts === 1 || attempts % 6 === 0) {
@@ -3881,18 +3964,6 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
3881
3964
  }
3882
3965
  }
3883
3966
 
3884
- if (!wakeListener) {
3885
- try {
3886
- wakeListener = await createHubWakeListener({
3887
- getAccessToken: async () => (await refreshSessionIfNeeded(supabase))?.access_token || ''
3888
- });
3889
- } catch (error) {
3890
- if (attempts === 1) {
3891
- console.warn(`[LiveDesk Client] Hub-online event channel unavailable; adaptive registry retries remain active: ${error?.message || error}`);
3892
- }
3893
- }
3894
- }
3895
-
3896
3967
  const trigger = await waitForDiscoveryTrigger(
3897
3968
  getDiscoveryRetryDelay(attempts, configuredIntervalMs),
3898
3969
  wakeListener?.promise,
@@ -3901,9 +3972,7 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
3901
3972
  if (trigger.type === 'hub-online') {
3902
3973
  wakeListener?.close();
3903
3974
  wakeListener = null;
3904
- const jitterMs = Math.floor(Math.random() * (HUB_WAKE_QUERY_JITTER_MAX_MS + 1));
3905
- console.log(`[LiveDesk Client] Hub-online event received. Rechecking the registry in ${jitterMs}ms.`);
3906
- await waitForDiscoveryTrigger(jitterMs, null, shouldStop);
3975
+ console.log('[LiveDesk Client] Hub-online event received. Rechecking the registry now.');
3907
3976
  }
3908
3977
  }
3909
3978
  } finally {
@@ -3993,7 +4062,7 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3993
4062
  resolved = await resolveManagerWithCachedRecovery(cacheOwnerKey, {
3994
4063
  allowRelayFallback,
3995
4064
  relayEndpoint: parsed.relay,
3996
- resolveFreshTarget: async () => {
4065
+ resolveFreshTarget: async signal => {
3997
4066
  // The signed registry is authoritative for Hub replacement
3998
4067
  // and pair-token rotation. Cache direct/relay recovery is
3999
4068
  // considered only after this one current lookup reports
@@ -4006,7 +4075,8 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
4006
4075
  console.warn('[LiveDesk Client] Another auth owner rotated the refresh token. Keeping the runtime alive while the saved session catches up.');
4007
4076
  }
4008
4077
  return await resolveManagerFromSupabase(supabase, {
4009
- allowRelayFallback
4078
+ allowRelayFallback,
4079
+ signal
4010
4080
  });
4011
4081
  }
4012
4082
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.206",
3
+ "version": "0.1.208",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,10 +40,10 @@
40
40
  "ws": "^8.18.3"
41
41
  },
42
42
  "optionalDependencies": {
43
- "@livedesk/fast-linux-x64": "0.1.412",
44
- "@livedesk/fast-osx-arm64": "0.1.412",
45
- "@livedesk/fast-osx-x64": "0.1.412",
46
- "@livedesk/fast-win-x64": "0.1.412"
43
+ "@livedesk/fast-linux-x64": "0.1.413",
44
+ "@livedesk/fast-osx-arm64": "0.1.413",
45
+ "@livedesk/fast-osx-x64": "0.1.413",
46
+ "@livedesk/fast-win-x64": "0.1.413"
47
47
  },
48
48
  "publishConfig": {
49
49
  "access": "public"
package/hub/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
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 {
@@ -42,69 +42,22 @@ export function createAgentManager({
42
42
 
43
43
  async function publicSettings() {
44
44
  const current = await settings.get();
45
- const {
46
- codexMaxTurns: _codexMaxTurns,
47
- provider: _provider,
48
- ...exposedSettings
49
- } = current;
50
45
  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
46
  return {
61
- ...exposedSettings,
47
+ enabled: current.enabled === true,
62
48
  codexInstallation: codex.installed ? 'installed' : 'not-installed',
63
49
  codexAuth: codex.authenticated,
64
- codexStatus: codex.status,
65
- codexPath: codex.codexPath,
66
- agentWorkspacePath: current.codexWorkingDirectory,
67
- securityStatus
50
+ codexStatus: codex.status
68
51
  };
69
52
  }
70
53
 
71
54
  return {
72
55
  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
56
  async testConnection() {
101
- if (!runtime) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
57
+ if (!runtime) throw new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
102
58
  statusCache = { expiresAt: 0, value: null };
103
59
  return runtime.testConnection();
104
60
  },
105
- async createPlan() {
106
- throw new AgentProviderError('agent-codex-run-required', 'Codex Agent requests use the LiveDesk tool loop.', { status: 409 });
107
- },
108
61
  async createSummary(input) {
109
62
  const source = input && typeof input === 'object' ? input : {};
110
63
  const results = Array.isArray(source.results) ? source.results : [];
@@ -113,9 +66,8 @@ export function createAgentManager({
113
66
  };
114
67
  },
115
68
  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 });
69
+ if (!runtime) {
70
+ throw new AgentRuntimeError('agent-codex-not-active', 'Codex SDK is not available for LiveDesk Agent commands.', { status: 409 });
119
71
  }
120
72
  return runtime.start(input);
121
73
  },
@@ -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 }));