@bitkyc08/opencodex 2.42.0 → 2.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -52,7 +52,7 @@ import {
52
52
  setDebugSettings,
53
53
  type DebugFlag,
54
54
  } from "../../lib/debug-settings";
55
- import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
55
+ import type { OcxClaudeCodeConfig, OcxComboConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
56
56
  import { drainAndShutdown } from "../lifecycle";
57
57
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
58
58
  import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
@@ -66,6 +66,7 @@ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, C
66
66
  import type { ManagementContext } from "./context";
67
67
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
68
68
  import { shadowCallTargetError } from "./shadow-call-validation";
69
+ import { COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS } from "../../combos";
69
70
 
70
71
 
71
72
  /**
@@ -75,15 +76,29 @@ import { shadowCallTargetError } from "./shadow-call-validation";
75
76
  * materialized in every user's config.json.
76
77
  */
77
78
  function sparseComboConfig<T extends {
79
+ cooldownMs?: number;
80
+ waitForCooldownMs?: number;
78
81
  imageInput?: "auto" | "disabled";
79
82
  reasoningEffortMode?: "strict" | "adaptive";
80
- }>(combo: T): Omit<T, "imageInput" | "reasoningEffortMode"> & {
83
+ }>(combo: T): Omit<T, "cooldownMs" | "waitForCooldownMs" | "imageInput" | "reasoningEffortMode"> & {
84
+ cooldownMs?: number;
85
+ waitForCooldownMs?: number;
81
86
  imageInput?: "disabled";
82
87
  reasoningEffortMode?: "adaptive";
83
88
  } {
84
- const { imageInput, reasoningEffortMode, ...rest } = combo;
89
+ const {
90
+ cooldownMs,
91
+ waitForCooldownMs,
92
+ imageInput,
93
+ reasoningEffortMode,
94
+ ...rest
95
+ } = combo;
85
96
  return {
86
97
  ...rest,
98
+ ...(cooldownMs !== undefined ? { cooldownMs } : {}),
99
+ ...(waitForCooldownMs !== undefined && waitForCooldownMs !== COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS
100
+ ? { waitForCooldownMs }
101
+ : {}),
87
102
  ...(imageInput === "disabled" ? { imageInput: "disabled" as const } : {}),
88
103
  ...(reasoningEffortMode === "adaptive" ? { reasoningEffortMode: "adaptive" as const } : {}),
89
104
  };
@@ -141,13 +156,28 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
141
156
  comboPublicModelId,
142
157
  normalizeComboConfig,
143
158
  } = await import("../../combos");
144
- const error = comboConfigError(id, body.combo, config.providers, {
159
+ const sourceId = renameFrom ?? id;
160
+ const previous = config.combos?.[sourceId];
161
+ if (!isPlainRecord(body.combo)) {
162
+ return jsonResponse({ error: "combo must be an object" }, 400);
163
+ }
164
+ const requestedCombo: Record<string, unknown> = body.combo;
165
+ const effectiveCombo = {
166
+ ...requestedCombo,
167
+ ...(!Object.hasOwn(requestedCombo, "cooldownMs") && previous?.cooldownMs !== undefined
168
+ ? { cooldownMs: previous.cooldownMs }
169
+ : {}),
170
+ ...(!Object.hasOwn(requestedCombo, "waitForCooldownMs") && previous?.waitForCooldownMs !== undefined
171
+ ? { waitForCooldownMs: previous.waitForCooldownMs }
172
+ : {}),
173
+ };
174
+ const error = comboConfigError(id, effectiveCombo, config.providers, {
145
175
  requireEnabledTarget: true,
146
176
  combos: config.combos,
147
- excludeComboId: renameFrom ?? id,
177
+ excludeComboId: sourceId,
148
178
  });
149
179
  if (error) return jsonResponse({ error }, 400);
150
- const normalized = normalizeComboConfig(body.combo as import("../../types").OcxComboConfig);
180
+ const normalized = normalizeComboConfig(effectiveCombo as unknown as OcxComboConfig);
151
181
  // Persist only non-default identity/capability fields so config stays sparse.
152
182
  // Capability defaults (`imageInput`, `reasoningEffortMode`) go through the same
153
183
  // helper the GET/PUT responses use, so the wire shape and the stored shape cannot drift.
@@ -157,14 +187,12 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
157
187
  displayName: normalizedDisplayName,
158
188
  ...normalizedBase
159
189
  } = sparseComboConfig(normalized);
160
- const stored: import("../../types").OcxComboConfig = {
190
+ const stored: OcxComboConfig = {
161
191
  ...normalizedBase,
162
192
  ...(normalizedAlias ? { alias: normalizedAlias } : {}),
163
193
  ...(normalizedNativeAlias ? { nativeAlias: true } : {}),
164
194
  ...(normalizedDisplayName ? { displayName: normalizedDisplayName } : {}),
165
195
  };
166
- const sourceId = renameFrom ?? id;
167
- const previous = config.combos?.[sourceId];
168
196
  const oldPublicModel = previous ? comboPublicModelId(sourceId, previous) : null;
169
197
  const newPublicModel = comboPublicModelId(id, normalized);
170
198
  const disabledIdentityChanged = previous !== undefined && (
@@ -41,6 +41,15 @@ import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../provid
41
41
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
42
42
  import { clearThreadAccountMap } from "../../codex/routing";
43
43
  import { primeCodexPoolQuotas } from "../../codex/auth-api";
44
+ import { isSelectableCodexPoolAccount } from "../../codex/account-id";
45
+ import { isCodexAccountPriorityKey } from "../../codex/account-priority";
46
+ import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
47
+ import { getAccountQuota } from "../../codex/quota";
48
+ import {
49
+ codexQuotaAutoRefreshStatus,
50
+ runCodexQuotaAutoRefresh,
51
+ } from "../../codex/quota-auto-refresh";
52
+ import { getMainAccountHardLockStatus } from "../../codex/main-account-hard-lock";
44
53
  import {
45
54
  codexAccountPickerEnabled,
46
55
  initializeDefaultCodexAccountNamespaces,
@@ -106,6 +115,13 @@ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, C
106
115
  import type { ManagementContext } from "./context";
107
116
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
108
117
 
118
+ function quotaAutoRefreshSettings(config: OcxConfig) {
119
+ return Object.fromEntries(Object.entries(config.codexQuotaAutoRefresh ?? {}).map(([id, setting]) => [
120
+ id,
121
+ { fiveHour: setting.fiveHour === true, weekly: setting.weekly === true },
122
+ ]));
123
+ }
124
+
109
125
  async function sidecarVisionResponseSettings(config: OcxConfig): Promise<{
110
126
  model: string;
111
127
  reasoning: string;
@@ -300,9 +316,15 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
300
316
  streamMode: config.streamMode ?? "auto",
301
317
  appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
302
318
  codexAccountPickerEnabled: codexAccountPickerEnabled(config),
319
+ codexQuotaAutoRefresh: quotaAutoRefreshSettings(config),
303
320
  // Absent means hidden, so the GUI renders the switch without having to know that
304
321
  // `undefined` and `false` mean the same thing.
305
322
  showCodexSparkQuota: config.showCodexSparkQuota === true,
323
+ // Absent means off, same convention: the GUI renders a plain switch without
324
+ // needing to know that `undefined` and `false` mean the same thing here.
325
+ ultraFastTier: config.ultraFastTier === true,
326
+ codexMainAccountHardLock: config.codexMainAccountHardLock === true,
327
+ mainAccountHardLock: getMainAccountHardLockStatus(config),
306
328
  // Absent means the historical auto-open, so the GUI can render the toggle
307
329
  // without having to know that `undefined` and `true` mean the same thing.
308
330
  oauthOpenBrowser: config.oauthOpenBrowser !== false,
@@ -392,18 +414,24 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
392
414
  streamMode?: unknown;
393
415
  appOwnedMemoryBudgetMb?: unknown;
394
416
  codexAccountPickerEnabled?: unknown;
417
+ codexQuotaAutoRefresh?: unknown;
395
418
  oauthOpenBrowser?: unknown;
396
419
  showCodexSparkQuota?: unknown;
420
+ ultraFastTier?: unknown;
421
+ codexMainAccountHardLock?: unknown;
397
422
  codexDesktopAuthless?: unknown;
398
423
  };
399
424
  if (body.codexAutoStart === undefined
400
425
  && body.streamMode === undefined
401
426
  && body.appOwnedMemoryBudgetMb === undefined
402
427
  && body.codexAccountPickerEnabled === undefined
428
+ && body.codexQuotaAutoRefresh === undefined
403
429
  && body.oauthOpenBrowser === undefined
404
430
  && body.showCodexSparkQuota === undefined
431
+ && body.ultraFastTier === undefined
432
+ && body.codexMainAccountHardLock === undefined
405
433
  && body.codexDesktopAuthless === undefined) {
406
- return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, oauthOpenBrowser, showCodexSparkQuota, or codexDesktopAuthless" }, 400);
434
+ return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, showCodexSparkQuota, ultraFastTier, codexMainAccountHardLock, or codexDesktopAuthless" }, 400);
407
435
  }
408
436
  if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
409
437
  return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
@@ -421,9 +449,36 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
421
449
  if (body.showCodexSparkQuota !== undefined && typeof body.showCodexSparkQuota !== "boolean") {
422
450
  return jsonResponse({ error: "showCodexSparkQuota boolean is required" }, 400);
423
451
  }
452
+ if (body.ultraFastTier !== undefined && typeof body.ultraFastTier !== "boolean") {
453
+ return jsonResponse({ error: "ultraFastTier boolean is required" }, 400);
454
+ }
455
+ if (body.codexMainAccountHardLock !== undefined && typeof body.codexMainAccountHardLock !== "boolean") {
456
+ return jsonResponse({ error: "codexMainAccountHardLock boolean is required" }, 400);
457
+ }
424
458
  if (body.codexDesktopAuthless !== undefined && typeof body.codexDesktopAuthless !== "boolean") {
425
459
  return jsonResponse({ error: "codexDesktopAuthless boolean is required" }, 400);
426
460
  }
461
+ let quotaAutoRefreshChange: { id: string; window: "fiveHour" | "weekly"; enabled: boolean } | undefined;
462
+ if (body.codexQuotaAutoRefresh !== undefined) {
463
+ if (!isPlainRecord(body.codexQuotaAutoRefresh)) {
464
+ return jsonResponse({ error: "codexQuotaAutoRefresh must be an object" }, 400);
465
+ }
466
+ const change = body.codexQuotaAutoRefresh;
467
+ const id = typeof change.id === "string" ? change.id.trim() : "";
468
+ if (!isCodexAccountPriorityKey(id)) return jsonResponse({ error: "Invalid account id format" }, 400);
469
+ if (change.window !== "fiveHour" && change.window !== "weekly") {
470
+ return jsonResponse({ error: "window must be fiveHour or weekly" }, 400);
471
+ }
472
+ if (typeof change.enabled !== "boolean") return jsonResponse({ error: "enabled must be a boolean" }, 400);
473
+ const exists = id === MAIN_CODEX_ACCOUNT_ID
474
+ || (config.codexAccounts ?? []).some(account => isSelectableCodexPoolAccount(account) && account.id === id);
475
+ if (!exists) return jsonResponse({ error: "Account not found" }, 404);
476
+ const status = codexQuotaAutoRefreshStatus(config, id, getAccountQuota(id));
477
+ if (change.enabled && !status[change.window === "fiveHour" ? "fiveHourAvailable" : "weeklyAvailable"]) {
478
+ return jsonResponse({ error: "Quota window is not available for this account" }, 409);
479
+ }
480
+ quotaAutoRefreshChange = { id, window: change.window, enabled: change.enabled };
481
+ }
427
482
  if (body.appOwnedMemoryBudgetMb !== undefined && (
428
483
  typeof body.appOwnedMemoryBudgetMb !== "number"
429
484
  || !Number.isInteger(body.appOwnedMemoryBudgetMb)
@@ -443,10 +498,16 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
443
498
  hasCodexAccountNamespaces: Object.hasOwn(config, "codexAccountNamespaces"),
444
499
  codexAccountPickerEnabled: config.codexAccountPickerEnabled,
445
500
  hasCodexAccountPickerEnabled: Object.hasOwn(config, "codexAccountPickerEnabled"),
501
+ codexQuotaAutoRefresh: config.codexQuotaAutoRefresh,
502
+ hasCodexQuotaAutoRefresh: Object.hasOwn(config, "codexQuotaAutoRefresh"),
446
503
  oauthOpenBrowser: config.oauthOpenBrowser,
447
504
  hasOauthOpenBrowser: Object.hasOwn(config, "oauthOpenBrowser"),
448
505
  showCodexSparkQuota: config.showCodexSparkQuota,
449
506
  hasShowCodexSparkQuota: Object.hasOwn(config, "showCodexSparkQuota"),
507
+ ultraFastTier: config.ultraFastTier,
508
+ hasUltraFastTier: Object.hasOwn(config, "ultraFastTier"),
509
+ codexMainAccountHardLock: config.codexMainAccountHardLock,
510
+ hasCodexMainAccountHardLock: Object.hasOwn(config, "codexMainAccountHardLock"),
450
511
  codexDesktopAuthless: config.codexDesktopAuthless,
451
512
  hasCodexDesktopAuthless: Object.hasOwn(config, "codexDesktopAuthless"),
452
513
  };
@@ -479,8 +540,25 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
479
540
  if (typeof body.showCodexSparkQuota === "boolean") {
480
541
  config.showCodexSparkQuota = body.showCodexSparkQuota;
481
542
  }
543
+ // Off deletes the key rather than persisting `false`: absent is the documented
544
+ // default, and a written `false` would survive as a decision nobody made.
545
+ if (body.ultraFastTier === true) config.ultraFastTier = true;
546
+ else if (body.ultraFastTier === false) deleteConfigTopLevelKey(config, "ultraFastTier");
547
+ if (body.codexMainAccountHardLock === true) config.codexMainAccountHardLock = true;
548
+ else if (body.codexMainAccountHardLock === false) deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
482
549
  if (body.codexDesktopAuthless === true) config.codexDesktopAuthless = true;
483
550
  else if (body.codexDesktopAuthless === false) deleteConfigTopLevelKey(config, "codexDesktopAuthless");
551
+ if (quotaAutoRefreshChange) {
552
+ const { id, window, enabled } = quotaAutoRefreshChange;
553
+ const setting = { ...(config.codexQuotaAutoRefresh?.[id] ?? {}) };
554
+ if (enabled) setting[window] = true;
555
+ else delete setting[window];
556
+ const all = { ...(config.codexQuotaAutoRefresh ?? {}) };
557
+ if (Object.keys(setting).length > 0) all[id] = setting;
558
+ else delete all[id];
559
+ if (Object.keys(all).length > 0) config.codexQuotaAutoRefresh = all;
560
+ else deleteConfigTopLevelKey(config, "codexQuotaAutoRefresh");
561
+ }
484
562
  pickerIsEnabled = codexAccountPickerEnabled(config);
485
563
  (deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
486
564
  } catch (error) {
@@ -497,12 +575,21 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
497
575
  if (previousSettings.hasCodexAccountPickerEnabled) {
498
576
  config.codexAccountPickerEnabled = previousSettings.codexAccountPickerEnabled;
499
577
  } else deleteConfigTopLevelKey(config, "codexAccountPickerEnabled");
578
+ if (previousSettings.hasCodexQuotaAutoRefresh) {
579
+ config.codexQuotaAutoRefresh = previousSettings.codexQuotaAutoRefresh;
580
+ } else deleteConfigTopLevelKey(config, "codexQuotaAutoRefresh");
500
581
  if (previousSettings.hasOauthOpenBrowser) {
501
582
  config.oauthOpenBrowser = previousSettings.oauthOpenBrowser;
502
583
  } else deleteConfigTopLevelKey(config, "oauthOpenBrowser");
503
584
  if (previousSettings.hasShowCodexSparkQuota) {
504
585
  config.showCodexSparkQuota = previousSettings.showCodexSparkQuota;
505
586
  } else deleteConfigTopLevelKey(config, "showCodexSparkQuota");
587
+ if (previousSettings.hasUltraFastTier) {
588
+ config.ultraFastTier = previousSettings.ultraFastTier;
589
+ } else deleteConfigTopLevelKey(config, "ultraFastTier");
590
+ if (previousSettings.hasCodexMainAccountHardLock) {
591
+ config.codexMainAccountHardLock = previousSettings.codexMainAccountHardLock;
592
+ } else deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
506
593
  if (previousSettings.hasCodexDesktopAuthless) {
507
594
  config.codexDesktopAuthless = previousSettings.codexDesktopAuthless;
508
595
  } else deleteConfigTopLevelKey(config, "codexDesktopAuthless");
@@ -522,16 +609,20 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
522
609
  ? catalogRefreshIsPending(catalogRefresh)
523
610
  : false;
524
611
  invalidateStartupHealthCache();
612
+ if (quotaAutoRefreshChange) void runCodexQuotaAutoRefresh(config);
525
613
  return jsonResponse({
526
614
  ok: true,
527
615
  codexAutoStart: codexAutoStartEnabled(config),
528
616
  streamMode: config.streamMode ?? "auto",
529
617
  appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
530
618
  codexAccountPickerEnabled: pickerIsEnabled,
619
+ codexQuotaAutoRefresh: quotaAutoRefreshSettings(config),
531
620
  oauthOpenBrowser: config.oauthOpenBrowser !== false,
532
621
  catalogRefreshPending,
533
622
  showCodexSparkQuota: config.showCodexSparkQuota === true,
534
623
  codexDesktopAuthless: authlessIsEnabled,
624
+ codexMainAccountHardLock: config.codexMainAccountHardLock === true,
625
+ mainAccountHardLock: getMainAccountHardLockStatus(config),
535
626
  startupHealth: await readStartupHealth(config),
536
627
  });
537
628
  }
@@ -679,7 +770,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
679
770
  }
680
771
  // Reject ONLY a model we can prove is blind. An id nothing knows about stays
681
772
  // allowed: the operator may be ahead of our catalog, and the runtime never
682
- // required catalog membership (`tests/vision-reasoning-contract.test.ts`
773
+ // required catalog membership (`tests/vision/vision-reasoning-contract.test.ts`
683
774
  // pins `custom-vision` → 200). The catalog is read ONCE and reused for the
684
775
  // rejection body, so a 400 cannot cost two provider fetches.
685
776
  if (body.vision && typeof body.vision.model === "string" && body.vision.model !== "") {
@@ -76,6 +76,15 @@ export interface IntegrationJournalRow {
76
76
  configPath: string;
77
77
  snapshot: "none" | "stored" | "expired";
78
78
  undoable: boolean;
79
+ /**
80
+ * May the operator retire this row?
81
+ *
82
+ * Computed HERE, not in the GUI, because the DELETE route enforces the same
83
+ * rule and two copies of it would drift. False for a client newest row: it
84
+ * is the undo entry point (`undoable` above keys off exactly this), and it
85
+ * is what a user reaches for right after the mistake.
86
+ */
87
+ deletable: boolean;
79
88
  }
80
89
 
81
90
  export interface IntegrationToggleBody {
@@ -293,6 +302,94 @@ function writerFailureResponse(
293
302
  }, 500, ctx.req, ctx.config);
294
303
  }
295
304
 
305
+ /**
306
+ * Who asked for this deletion, as an audit value that is safe to persist.
307
+ *
308
+ * The tombstone lives in an append-only log the user can read, so this must be
309
+ * a principal NAME and nothing else -- never the admin token, a session id, or
310
+ * a filesystem path. `principal` is undefined only in direct-dispatch tests,
311
+ * which the auth gate documents as the untrusted admin-token case.
312
+ */
313
+ function journalDeletePrincipal(ctx: ManagementContext): string {
314
+ return ctx.principal ?? "admin-token";
315
+ }
316
+
317
+ /**
318
+ * Retire one journal row at the operator request.
319
+ *
320
+ * The opId travels in the QUERY STRING, matching DELETE
321
+ * /api/codex-auth/accounts?id= -- the repository other DELETE-by-identifier. A
322
+ * body on DELETE is legal but unevenly handled by intermediaries, and there is
323
+ * nothing here a query cannot carry.
324
+ */
325
+ async function handleJournalDelete(ctx: ManagementContext): Promise<Response> {
326
+ const { req, url } = ctx;
327
+ const opId = url.searchParams.get("opId")?.trim();
328
+ if (!opId) {
329
+ return jsonResponse({
330
+ error: "opId must be a non-empty string",
331
+ code: "invalid_op_id",
332
+ }, 400, req, ctx.config);
333
+ }
334
+ try {
335
+ const store = integrationStore();
336
+ const operation = store.findOperation(opId);
337
+ if (!operation) {
338
+ // Already retired, or never existed. Both are 404: the tombstone hides
339
+ // the row from findOperation, so a double-click is idempotent here
340
+ // rather than a second deletion of something.
341
+ return jsonResponse({
342
+ error: "integration operation not found",
343
+ code: "integration_operation_not_found",
344
+ opId,
345
+ }, 404, req, ctx.config);
346
+ }
347
+ /*
348
+ * The newest row per client is refused, and refused by the SERVER even
349
+ * though the GUI already hides its button. The button is a courtesy; this
350
+ * is the rule. An admin-token caller has no GUI at all.
351
+ *
352
+ * Re-read immediately before the write: a restore that landed while the
353
+ * dialog was open appends a new row and changes which opId is newest.
354
+ */
355
+ const newest = store.listOperations(operation.clientId, 1)[0];
356
+ if (newest?.opId === opId) {
357
+ return jsonResponse({
358
+ error: "the newest operation for a client cannot be deleted",
359
+ code: "integration_journal_newest_protected",
360
+ clientId: operation.clientId,
361
+ opId,
362
+ }, 409, req, ctx.config);
363
+ }
364
+
365
+ store.retireOperation({
366
+ tombstone: opId,
367
+ at: new Date().toISOString(),
368
+ by: journalDeletePrincipal(ctx),
369
+ });
370
+
371
+ /*
372
+ * Snapshot bytes go too, and go AFTER the tombstone -- the same post-commit
373
+ * ordering appendOperation uses (journal.ts rule 1). If this fails, the row
374
+ * is still retired and retentionDegraded discloses the leftover file; the
375
+ * reverse order would delete a user backup for a deletion that then failed
376
+ * to record.
377
+ */
378
+ const pruned = store.pruneSnapshots(operation.clientId);
379
+ if (pruned.ok) store.clearPruneFailure(operation.clientId);
380
+ else store.markPruneFailure(operation.clientId, pruned.error);
381
+
382
+ return jsonResponse({
383
+ ok: true,
384
+ opId,
385
+ clientId: operation.clientId,
386
+ snapshotRemoved: pruned.ok,
387
+ }, 200, req, ctx.config);
388
+ } catch (error) {
389
+ return internalErrorResponse(error, ctx);
390
+ }
391
+ }
392
+
296
393
  export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<Response | null> {
297
394
  const { req, url } = ctx;
298
395
 
@@ -319,6 +416,10 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
319
416
  }
320
417
  }
321
418
 
419
+ if (url.pathname === "/api/client-integrations/journal" && req.method === "DELETE") {
420
+ return handleJournalDelete(ctx);
421
+ }
422
+
322
423
  if (url.pathname === "/api/client-integrations/journal") {
323
424
  if (req.method !== "GET") return null;
324
425
  const requestedClient = url.searchParams.get("client");
@@ -371,6 +472,13 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
371
472
  const current = currentConfigText(operation.configPath);
372
473
  return current === undefined ? false : matchesOperationResult(operation, current);
373
474
  })(),
475
+ /*
476
+ * Deliberately NOT an `undoable` derivative. The two axes are
477
+ * independent: an expired row is undoable-false and deletable-true,
478
+ * which is the pairing this route exists to produce -- a row whose
479
+ * bytes are gone previously carried no action at all.
480
+ */
481
+ deletable: newestByClient.get(operation.clientId) !== operation.opId,
374
482
  };
375
483
  });
376
484
  return jsonResponse({ operations } satisfies IntegrationJournalEnvelope, 200, req, ctx.config);
@@ -154,6 +154,7 @@ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostR
154
154
  import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
155
155
  import type { ManagementContext } from "./context";
156
156
  import { listManagementModelRows, loadExportModels } from "./model-rows";
157
+ import { initialModelSelectionPending } from "../../providers/initial-model-selection";
157
158
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
158
159
  import {
159
160
  hasModelPreset,
@@ -536,6 +537,9 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
536
537
  }
537
538
 
538
539
  const providerConfig = hasOwnProvider(config.providers, provider) ? config.providers[provider] : undefined;
540
+ if (initialModelSelectionPending(providerConfig)) {
541
+ return jsonResponse({ error: "Initial model discovery is pending. Refresh the model list and retry.", code: "initial_model_selection_pending" }, 409);
542
+ }
539
543
  const isVirtualComboNamespace = provider === COMBO_NAMESPACE && !preservesPhysicalComboProvider(config);
540
544
  if (!providerConfig && provider !== "openai" && !isVirtualComboNamespace) {
541
545
  return jsonResponse({ error: "unknown model visibility provider" }, 400);
@@ -836,7 +840,13 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
836
840
  if (mode !== "preset" && mode !== "all" && mode !== "custom") {
837
841
  return jsonResponse({ error: "mode must be preset, all, or custom" }, 400);
838
842
  }
843
+ if (mode === "preset" && !hasModelPreset(provider)) {
844
+ return jsonResponse({ error: `no model preset is shipped for provider '${provider}'` }, 400);
845
+ }
839
846
  const target = config.providers[provider];
847
+ if (initialModelSelectionPending(target)) {
848
+ return jsonResponse({ error: "Initial model discovery is pending. Refresh the model list and retry.", code: "initial_model_selection_pending" }, 409);
849
+ }
840
850
  if (mode === "all") {
841
851
  // Same effect as today's empty-list PUT: no allowlist, no marker to reconcile.
842
852
  delete target.selectedModels;
@@ -851,9 +861,6 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
851
861
  persistConfig(config);
852
862
  return jsonResponse({ ok: true, provider, mode, selected: [...(target.selectedModels ?? [])] });
853
863
  }
854
- if (!hasModelPreset(provider)) {
855
- return jsonResponse({ error: `no model preset is shipped for provider '${provider}'` }, 400);
856
- }
857
864
  const models = await fetchAllModels(config);
858
865
  const catalogIds = models.filter(m => m.provider === provider).map(m => m.id);
859
866
  const presetIds = materializeModelPreset(provider, catalogIds);
@@ -900,6 +907,9 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
900
907
  if (!provider || !hasOwnProvider(config.providers, provider)) {
901
908
  return jsonResponse({ error: "unknown provider" }, provider ? 404 : 400);
902
909
  }
910
+ if (initialModelSelectionPending(config.providers[provider])) {
911
+ return jsonResponse({ error: "Initial model discovery is pending. Refresh the model list and retry.", code: "initial_model_selection_pending" }, 409);
912
+ }
903
913
  const models = Array.isArray(body.models)
904
914
  ? [...new Set(body.models.filter((m): m is string => typeof m === "string"))]
905
915
  : [];
@@ -27,6 +27,9 @@ import { routedSlug, slugEquals } from "../../providers/slug-codec";
27
27
  import type { OcxConfig } from "../../types";
28
28
  import { ensureCodexEntitlementFreshness } from "../../codex/model-entitlements";
29
29
  import { fetchAllModels } from "./shared";
30
+ import { initialModelSelectionPending } from "../../providers/initial-model-selection";
31
+ import { catalogFastRowEligible, fastRowId } from "../fast-row";
32
+ import { knownEffortRowIds } from "../effort-row";
30
33
 
31
34
  /**
32
35
  * One row of the `/api/models` list. Routed rows spread a `CatalogModel`, so the shape is
@@ -38,9 +41,11 @@ export type ManagementModelRow = Partial<CatalogModel> & {
38
41
  id: string;
39
42
  namespaced: string;
40
43
  disabled: boolean;
44
+ initialSelectionPending?: boolean;
41
45
  native?: boolean;
42
46
  custom?: boolean;
43
47
  customId?: string;
48
+ fastRowAvailable?: boolean;
44
49
  displayNameOverride?: string;
45
50
  displayNameSource?: "operator" | "provider" | "fallback";
46
51
  };
@@ -164,7 +169,20 @@ export async function listManagementModelRows(
164
169
  ...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
165
170
  };
166
171
  }).filter((row): row is ManagementModelRow => row !== null);
167
- return [...native, ...dedupedRouted, ...visibleCustomModels];
172
+ const rows = [...native, ...dedupedRouted, ...visibleCustomModels];
173
+ // Include disabled rows and configured aliases before the export visibility filter:
174
+ // a hidden real `x--fast` must never become a synthetic selector for another model.
175
+ const knownIds = config.fastRows === false ? new Set<string>() : knownEffortRowIds(config);
176
+ for (const row of rows) knownIds.add(row.namespaced);
177
+ return rows.map(row => {
178
+ const pending = initialModelSelectionPending(config.providers[row.provider]);
179
+ return {
180
+ ...row,
181
+ ...(pending ? { disabled: true, initialSelectionPending: true } : {}),
182
+ fastRowAvailable: !row.disabled && !pending
183
+ && !knownIds.has(fastRowId(row.namespaced)) && catalogFastRowEligible(config, row),
184
+ };
185
+ });
168
186
  }
169
187
 
170
188
  /** `/api/models` row → the narrower input the client-config serializers accept. */
@@ -173,6 +191,7 @@ export function toExportModel(row: ManagementModelRow): ExportModel {
173
191
  namespaced: row.namespaced,
174
192
  provider: row.provider,
175
193
  id: row.id,
194
+ fastRowAvailable: row.fastRowAvailable === true,
176
195
  ...(row.native ? { native: true } : {}),
177
196
  ...(row.displayName && row.displayNameSource !== "fallback" ? { displayName: row.displayName } : {}),
178
197
  ...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
@@ -17,9 +17,11 @@
17
17
  * Design of record: devlog/_fin/260803_integrations_toggle_all/030 (routes),
18
18
  * 011 (Claude Code), 012 (Grok).
19
19
  */
20
+ import { join } from "node:path";
20
21
  import { loadConfig, saveConfigPreservingClaudeCode } from "../../config";
21
22
  import { readRuntimePort } from "../../config/process-state";
22
23
  import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeContextLimits } from "../../codex/catalog";
24
+ import { getCodexHome } from "../../codex/paths";
23
25
  import { providerContextCap } from "../../providers/context-cap";
24
26
  import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
25
27
  import { inspectDesktop3pConfigLibrary, removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p";
@@ -682,8 +684,9 @@ export async function handleNativeIntegrationRoutes(ctx: ManagementContext): Pro
682
684
 
683
685
  if (url.pathname === "/api/native-integrations" && req.method === "GET") {
684
686
  const { getConfigPath } = await import("../../config");
687
+ const codexConfigPath = join(getCodexHome(), "config.toml");
685
688
  return jsonResponse({
686
- clients: [claudeStatus(config, getConfigPath()), grokStatus(config), codexStatus(config, getConfigPath()), desktopStatus(config)],
689
+ clients: [claudeStatus(config, getConfigPath()), grokStatus(config), codexStatus(config, codexConfigPath), desktopStatus(config)],
687
690
  } satisfies NativeStatusListEnvelope);
688
691
  }
689
692