@bitkyc08/opencodex 2.79.0 → 2.81.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 (245) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/App-7yhoys8a.js +54 -0
  3. package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
  4. package/gui/dist/assets/index-RsoaAxXY.js +94 -0
  5. package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/gui/dist/provider-icons/claude-green.svg +1 -0
  8. package/package.json +3 -2
  9. package/src/adapters/anthropic-model-contract.ts +8 -2
  10. package/src/adapters/anthropic.ts +7 -2
  11. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  12. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  13. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  14. package/src/adapters/coding-agent/protocol.ts +122 -17
  15. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  16. package/src/adapters/coding-agent/turn.ts +101 -44
  17. package/src/adapters/cursor/catalog.ts +8 -0
  18. package/src/adapters/cursor/effort-map.ts +2 -0
  19. package/src/adapters/cursor/live-transport.ts +5 -3
  20. package/src/adapters/cursor/native-exec-fs.ts +12 -9
  21. package/src/adapters/cursor/native-exec.ts +13 -5
  22. package/src/adapters/cursor/protobuf-request.ts +13 -7
  23. package/src/adapters/cursor/request-builder.ts +23 -10
  24. package/src/adapters/cursor/tool-definitions.ts +9 -6
  25. package/src/adapters/cursor/tool-guidance.ts +29 -8
  26. package/src/adapters/cursor/tool-naming.ts +40 -7
  27. package/src/adapters/cursor/tool-wording.ts +46 -0
  28. package/src/adapters/cursor.ts +3 -0
  29. package/src/adapters/devin/live-models.ts +2 -0
  30. package/src/adapters/devin.ts +25 -11
  31. package/src/adapters/kiro-retry.ts +9 -4
  32. package/src/adapters/openai-chat/messages.ts +7 -4
  33. package/src/adapters/physical-send.ts +8 -5
  34. package/src/adapters/qoder/adapter.ts +41 -6
  35. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  36. package/src/adapters/zed.ts +16 -1
  37. package/src/claude/inbound-cache-stabilize.ts +4 -9
  38. package/src/claude/inbound.ts +10 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-listener.ts +71 -4
  41. package/src/cli/account-anthropic-threshold.ts +14 -5
  42. package/src/cli/account-api.ts +3 -1
  43. package/src/cli/account-extended.ts +13 -9
  44. package/src/cli/account-policy-dto.ts +1 -1
  45. package/src/cli/account-policy.ts +13 -4
  46. package/src/cli/account.ts +3 -3
  47. package/src/cli/capabilities-accounts.ts +2 -2
  48. package/src/cli/capabilities-agents-routing.ts +24 -0
  49. package/src/cli/capabilities-integrations.ts +16 -0
  50. package/src/cli/codex-shim-autorestore.ts +3 -0
  51. package/src/cli/dispatch.ts +10 -1
  52. package/src/cli/help.ts +4 -1
  53. package/src/cli/index.ts +11 -5
  54. package/src/cli/integration-plan-dto.ts +1 -0
  55. package/src/cli/integration-preview.ts +8 -1
  56. package/src/cli/integrations.ts +32 -3
  57. package/src/cli/message-args.ts +34 -0
  58. package/src/cli/message-command.ts +43 -0
  59. package/src/cli/message-runtime.ts +6 -0
  60. package/src/cli/observe.ts +3 -1
  61. package/src/cli/provider.ts +29 -6
  62. package/src/cli/registry.ts +24 -2
  63. package/src/clients/config-export/commandcode.ts +131 -0
  64. package/src/clients/config-export/contracts.ts +12 -0
  65. package/src/clients/config-export.ts +64 -1
  66. package/src/codex/auth-api/account-list.ts +11 -3
  67. package/src/codex/auth-api/main-account-probe.ts +25 -8
  68. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  69. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  70. package/src/codex/auth-api/routes.ts +1 -0
  71. package/src/codex/autostart-health.ts +3 -3
  72. package/src/codex/catalog/auto-review.ts +13 -6
  73. package/src/codex/catalog/build-entries.ts +15 -7
  74. package/src/codex/catalog/control-plane.ts +45 -0
  75. package/src/codex/catalog/effort.ts +11 -1
  76. package/src/codex/catalog/gather-capture.ts +5 -3
  77. package/src/codex/catalog/parsing.ts +2 -1
  78. package/src/codex/catalog/provider-models.ts +15 -9
  79. package/src/codex/control-plane-models.ts +6 -0
  80. package/src/codex/journal.ts +36 -5
  81. package/src/codex/shim-overlay.ts +9 -1
  82. package/src/combos/jev-decision-contract.ts +25 -2
  83. package/src/combos/jev.ts +6 -3
  84. package/src/combos/types.ts +1 -1
  85. package/src/config/diagnostics.ts +27 -16
  86. package/src/config/load-degrade.ts +34 -0
  87. package/src/config/schema/anthropic-account-pool.ts +143 -0
  88. package/src/config/schema/config-schema.ts +7 -1
  89. package/src/config/schema/leaf-validators.ts +4 -0
  90. package/src/config.ts +2 -0
  91. package/src/generated/compatibility-version.json +347 -199
  92. package/src/generated/model-metadata.ts +5 -4
  93. package/src/images/loop.ts +2 -1
  94. package/src/integrations/catalog-refresh.ts +1 -1
  95. package/src/integrations/mutation-plan.ts +7 -0
  96. package/src/integrations/registry.ts +8 -0
  97. package/src/integrations/state.ts +9 -2
  98. package/src/integrations/writer.ts +8 -2
  99. package/src/lib/lab-live-route-production.ts +9 -1
  100. package/src/lib/request-execution-budget.ts +236 -58
  101. package/src/lib/spend-pool-alias-validation.ts +79 -0
  102. package/src/lib/spend-pool-continuity.ts +34 -0
  103. package/src/lib/spend-reservation-ledger.ts +412 -50
  104. package/src/lib/state-store-registrations.ts +20 -3
  105. package/src/lib/upstream-retry.ts +17 -5
  106. package/src/lib/workflow-budget.ts +53 -11
  107. package/src/messaging/budget.ts +45 -0
  108. package/src/messaging/discovery.ts +57 -0
  109. package/src/messaging/envelope.ts +49 -0
  110. package/src/messaging/input.ts +37 -0
  111. package/src/messaging/rpc.ts +196 -0
  112. package/src/messaging/send.ts +58 -0
  113. package/src/messaging/socket.ts +72 -0
  114. package/src/messaging/types.ts +29 -0
  115. package/src/oauth/anthropic-account-refusal.ts +51 -17
  116. package/src/oauth/anthropic-account-threshold.ts +8 -2
  117. package/src/oauth/anthropic-continuity.ts +15 -1
  118. package/src/oauth/anthropic-model-quota.ts +120 -48
  119. package/src/oauth/anthropic-model-routes.ts +11 -3
  120. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  121. package/src/oauth/anthropic-pool-config.ts +30 -0
  122. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  123. package/src/oauth/anthropic-routing.ts +1202 -866
  124. package/src/oauth/anthropic-send-ownership.ts +41 -0
  125. package/src/oauth/anthropic.ts +4 -1
  126. package/src/oauth/generic-account-failover.ts +3 -1
  127. package/src/oauth/health.ts +8 -6
  128. package/src/oauth/index.ts +90 -173
  129. package/src/oauth/kiro.ts +22 -9
  130. package/src/oauth/login-cli.ts +2 -0
  131. package/src/oauth/model-discovery-auth.ts +43 -0
  132. package/src/oauth/pool-kernel.ts +11 -5
  133. package/src/oauth/pool-settings-capability.ts +5 -3
  134. package/src/oauth/store-anthropic-instance.ts +87 -0
  135. package/src/oauth/store.ts +67 -9
  136. package/src/oauth/token-guardian.ts +2 -0
  137. package/src/oauth/types.ts +2 -0
  138. package/src/protocols/plan-snapshot.ts +5 -0
  139. package/src/protocols/settings.ts +20 -5
  140. package/src/providers/anthropic-instance-id.ts +47 -0
  141. package/src/providers/anthropic-instance.ts +48 -0
  142. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  143. package/src/providers/derive.ts +2 -0
  144. package/src/providers/kiro-models.ts +3 -0
  145. package/src/providers/label.ts +1 -1
  146. package/src/providers/model-rename-fields.ts +2 -0
  147. package/src/providers/quota/account-cache.ts +37 -11
  148. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  149. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  150. package/src/providers/quota/report-cache.ts +24 -1
  151. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  152. package/src/providers/quota.ts +22 -32
  153. package/src/providers/registry/entries-core.ts +35 -30
  154. package/src/providers/registry/model-ids.ts +1 -0
  155. package/src/providers/registry/model-seeds.ts +7 -4
  156. package/src/providers/registry/types.ts +3 -0
  157. package/src/providers/registry.ts +3 -3
  158. package/src/responses/spill-store.ts +5 -1
  159. package/src/responses/state/metrics.ts +87 -0
  160. package/src/responses/state/snapshot-codec.ts +6 -0
  161. package/src/responses/state/spill-queue.ts +1 -0
  162. package/src/responses/state/unforced-store-false.ts +55 -0
  163. package/src/responses/state.ts +50 -66
  164. package/src/router.ts +28 -4
  165. package/src/routing/compatibility/assemble.ts +1 -1
  166. package/src/routing/quota.ts +7 -3
  167. package/src/server/auth-cors.ts +5 -0
  168. package/src/server/chat-completions.ts +6 -1
  169. package/src/server/chat-native.ts +45 -25
  170. package/src/server/claude-messages.ts +16 -2
  171. package/src/server/gui-static.ts +38 -4
  172. package/src/server/index/spend-ledger-lifecycle.ts +10 -4
  173. package/src/server/index.ts +16 -16
  174. package/src/server/inference/context.ts +1 -1
  175. package/src/server/lifecycle.ts +1 -1
  176. package/src/server/management/agent-settings-routes.ts +30 -6
  177. package/src/server/management/anthropic-account-threshold.ts +12 -9
  178. package/src/server/management/anthropic-pool-settings.ts +72 -0
  179. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  180. package/src/server/management/config-routes.ts +27 -7
  181. package/src/server/management/decision-routes.ts +2 -1
  182. package/src/server/management/integration-routes.ts +25 -2
  183. package/src/server/management/oauth-account-routes.ts +79 -98
  184. package/src/server/management/provider-instance-ownership.ts +44 -0
  185. package/src/server/management/provider-routes.ts +21 -28
  186. package/src/server/management/route-registry.ts +3 -1
  187. package/src/server/management/routing-profile-routes.ts +4 -4
  188. package/src/server/management/system-restart.ts +44 -14
  189. package/src/server/management/system-routes.ts +24 -2
  190. package/src/server/management/vision-sidecar-options.ts +9 -6
  191. package/src/server/management/web-search-sidecar-options.ts +51 -2
  192. package/src/server/management-auth.ts +6 -0
  193. package/src/server/messages-native-eligibility.ts +3 -2
  194. package/src/server/messages-native-oauth.ts +86 -41
  195. package/src/server/messages-native-scope.ts +4 -1
  196. package/src/server/messages-native-selector.ts +39 -0
  197. package/src/server/messages-native.ts +84 -25
  198. package/src/server/request-log.ts +5 -0
  199. package/src/server/responses/adapter-continuation.ts +29 -17
  200. package/src/server/responses/adapter-dispatch.ts +47 -17
  201. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  202. package/src/server/responses/combo-requested-effort.ts +14 -0
  203. package/src/server/responses/compact.ts +25 -8
  204. package/src/server/responses/compaction-recovery.ts +1 -1
  205. package/src/server/responses/core-combo-native.ts +4 -1
  206. package/src/server/responses/core-combo.ts +21 -18
  207. package/src/server/responses/core-normalize.ts +1 -0
  208. package/src/server/responses/core-options.ts +3 -1
  209. package/src/server/responses/core-replay.ts +5 -0
  210. package/src/server/responses/passthrough-delivery.ts +7 -3
  211. package/src/server/responses/passthrough-dispatch.ts +21 -8
  212. package/src/server/responses/request-prepare.ts +37 -5
  213. package/src/server/responses/request-send-budget.ts +74 -37
  214. package/src/server/responses/request-spend.ts +325 -33
  215. package/src/server/responses/request-transport.ts +132 -52
  216. package/src/server/responses/run-turn-execution.ts +11 -3
  217. package/src/server/responses/sidecar-execution.ts +96 -14
  218. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  219. package/src/server/workflow-refusal.ts +30 -0
  220. package/src/service/desktop-startup.ts +100 -3
  221. package/src/service/ownership-mutation-lease.d.mts +1 -1
  222. package/src/service/ownership-mutation-lease.mjs +5 -7
  223. package/src/sidecar/anthropic-binding.ts +79 -0
  224. package/src/sidecar/auth.ts +76 -2
  225. package/src/types/anthropic-account-pool.ts +33 -0
  226. package/src/types/config.ts +12 -28
  227. package/src/types/provider.ts +5 -0
  228. package/src/types/tools.ts +7 -0
  229. package/src/types.ts +4 -0
  230. package/src/usage/cost.ts +3 -2
  231. package/src/usage/expected-prices.ts +66 -14
  232. package/src/vision/anthropic-describe.ts +16 -3
  233. package/src/vision/index.ts +3 -0
  234. package/src/vision/plan.ts +19 -6
  235. package/src/web-search/alpha-search.ts +7 -1
  236. package/src/web-search/anthropic-executor.ts +16 -4
  237. package/src/web-search/header-deadline.ts +67 -0
  238. package/src/web-search/index.ts +7 -3
  239. package/src/web-search/loop.ts +32 -10
  240. package/src/web-search/passthrough-bridge.ts +4 -1
  241. package/src/web-search/sidecar-providers.ts +4 -3
  242. package/gui/dist/assets/App-BOUa-ek3.js +0 -54
  243. package/gui/dist/assets/Tray-DUtzB4zF.js +0 -1
  244. package/gui/dist/assets/index-DwHNDF4s.js +0 -94
  245. package/gui/dist/assets/quota-summary-BuL4WIBo.js +0 -1
@@ -21,7 +21,7 @@
21
21
  */
22
22
  import { homedir } from "node:os";
23
23
  import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
24
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
24
+ import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
25
25
  import { shouldInjectApiAuthHeader, standaloneCodexRoutingTarget } from "../codex/inject";
26
26
  import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
27
27
  import { canonicalizeReasoningEfforts } from "../reasoning-effort";
@@ -69,6 +69,18 @@ import { buildKiloClientConfig, summarizeKilo, buildKiloContribution, kiloConfig
69
69
  export { kiloConfigPath, kiloHomeDir, kiloCandidatePath, KILO_CONFIG_CANDIDATES } from "./config-export/kilo";
70
70
  export type { KiloGeneratedConfig, KiloProviderBlock, KiloModelEntry } from "./config-export/kilo";
71
71
  import { droidConfigPath, buildDroidClientConfig, summarizeDroid, buildDroidContribution } from "./config-export/droid";
72
+ import {
73
+ buildCommandCodeClientConfig,
74
+ summarizeCommandCode,
75
+ buildCommandCodeContribution,
76
+ commandCodeProviderRoot,
77
+ type CommandCodeGeneratedConfig,
78
+ type CommandCodeModelEntry,
79
+ type CommandCodeProviderBlock,
80
+ } from "./config-export/commandcode";
81
+
82
+ export type { CommandCodeGeneratedConfig, CommandCodeModelEntry, CommandCodeProviderBlock };
83
+ export { buildCommandCodeClientConfig, summarizeCommandCode, buildCommandCodeContribution, commandCodeProviderRoot };
72
84
 
73
85
 
74
86
 
@@ -474,6 +486,43 @@ export function zcodeConfigPath(env: OpencodeLaunchEnv = process.env, home: stri
474
486
  return join(zcodeHomeDir(env, home), "v2", "config.json");
475
487
  }
476
488
 
489
+ /**
490
+ * The home directory Command Code itself resolves: `env.HOME ?? env.USERPROFILE`
491
+ * (`homeDir15` in the published `command-code@1.66.0` bundle).
492
+ *
493
+ * On POSIX `os.homedir()` already reads `$HOME`, so the two agree and `home` is
494
+ * returned as is. On Windows `os.homedir()` reads `%USERPROFILE%` and ignores
495
+ * `HOME`, which Git for Windows, MSYS2 and Cygwin set, so a user whose `HOME` differs
496
+ * would get a provider block in a store Command Code never opens. Only the OS-default
497
+ * home is replaced: a caller that injects a different root (a test, the writer's
498
+ * `input.home`) keeps it, and a blank or relative value falls back to `home`.
499
+ */
500
+ export function commandCodeUserHome(env: OpencodeLaunchEnv = process.env, home: string = homedir(), platform: NodeJS.Platform = process.platform): string {
501
+ if (platform !== "win32" || home !== homedir()) return home;
502
+ const value = env.HOME ?? env.USERPROFILE;
503
+ return typeof value === "string" && value.trim() && win32.isAbsolute(value) ? value : home;
504
+ }
505
+
506
+ /**
507
+ * Command Code's provider store directory — `.commandcode` under the home the client
508
+ * resolves (see {@link commandCodeUserHome}), with no override.
509
+ *
510
+ * There is deliberately no `COMMANDCODE_HOME` here, unlike the other clients in this
511
+ * file: `grep -c COMMANDCODE_HOME` over the shipped `dist/cli.mjs` and
512
+ * `dist/index.mjs` returns 0. An override we honour but the client ignores is worse
513
+ * than none: `enable` would report success and write a provider block at a path no
514
+ * Command Code process ever opens, and the user would see an empty model list with no
515
+ * error anywhere. An apply against a relocated home is instead refused by `detectDir`
516
+ * not existing.
517
+ */
518
+ export function commandCodeHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir(), platform: NodeJS.Platform = process.platform): string {
519
+ return join(commandCodeUserHome(env, home, platform), ".commandcode");
520
+ }
521
+
522
+ export function commandCodeConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir(), platform: NodeJS.Platform = process.platform): string {
523
+ return join(commandCodeHomeDir(env, home, platform), "providers.json");
524
+ }
525
+
477
526
  /**
478
527
  * The provider store a current ZCode reads, which is NOT the file above.
479
528
  *
@@ -1487,6 +1536,20 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
1487
1536
  // remote binds refuse — same reasoning as MCode.
1488
1537
  loopbackOnly: true,
1489
1538
  },
1539
+ commandcode: {
1540
+ id: "commandcode",
1541
+ filename: "providers.json",
1542
+ destination: env => commandCodeConfigPath(env),
1543
+ // No env var exists behind this integration: buildCommandCodeClientConfig
1544
+ // writes `apiKey: false`, the documented keyless form for a loopback endpoint.
1545
+ apiKeyEnv: "",
1546
+ exportHint: "The provider is written with `apiKey: false` — Command Code treats a keyless loopback endpoint as already authenticated. No key is stored or referenced.",
1547
+ build: buildCommandCodeClientConfig,
1548
+ format: "json",
1549
+ summarize: summarizeCommandCode,
1550
+ buildContribution: buildCommandCodeContribution,
1551
+ loopbackOnly: true,
1552
+ },
1490
1553
  prime: {
1491
1554
  id: "prime",
1492
1555
  filename: "prime-models.json",
@@ -252,6 +252,13 @@ export function reconcileFreshPoolAccountPlans(runtimeConfig: OcxConfig, updates
252
252
  }
253
253
  }
254
254
 
255
+ export interface CodexAuthAccountsListOptions {
256
+ /** Finish deferred registration; only a dashboard session carries that inference consent. */
257
+ validatePending?: boolean;
258
+ /** An operator's explicit refresh command, which may retry a grant already recorded as dead. */
259
+ explicitRefresh?: boolean;
260
+ }
261
+
255
262
  export interface CodexAuthAccountsSnapshot {
256
263
  accounts: CodexAuthAccountDto[];
257
264
  mainIdentityGeneration: number;
@@ -260,7 +267,7 @@ export interface CodexAuthAccountsSnapshot {
260
267
  export async function listCodexAuthAccountsSnapshot(
261
268
  config: OcxConfig,
262
269
  forceRefresh = false,
263
- options: { validatePending?: boolean } = {},
270
+ options: CodexAuthAccountsListOptions = {},
264
271
  ): Promise<CodexAuthAccountsSnapshot> {
265
272
  const runtimeConfig = getRuntimeConfig(config);
266
273
  const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
@@ -281,7 +288,8 @@ export async function listCodexAuthAccountsSnapshot(
281
288
  // identity when the switch is on and nothing has been observed yet.
282
289
  const creditsIdentity = runtimeConfig.showCodexCredits === true ? poolQuotaHistoryIdentity(account.id) ?? null : null;
283
290
  const creditsUnobserved = creditsIdentity !== null && !hasCodexCreditsObservation(account.id, creditsIdentity);
284
- quotaResult = await fetchPoolAccountQuota(account.id, forceRefresh || creditsUnobserved, account.plan, getValidCodexToken, options.validatePending === true);
291
+ quotaResult = await fetchPoolAccountQuota(account.id, forceRefresh || creditsUnobserved, account.plan, getValidCodexToken,
292
+ options.validatePending === true, undefined, undefined, options.explicitRefresh === true);
285
293
  } catch (error) {
286
294
  if (!(error instanceof PoolQuotaProbeBusyError)) throw error;
287
295
  quotaResult = {
@@ -447,7 +455,7 @@ export async function refreshCodexQuotaForActivation(config: OcxConfig, accountI
447
455
  export async function listCodexAuthAccounts(
448
456
  config: OcxConfig,
449
457
  forceRefresh = false,
450
- options: { validatePending?: boolean } = {},
458
+ options: CodexAuthAccountsListOptions = {},
451
459
  ): Promise<CodexAuthAccountDto[]> {
452
460
  return (await listCodexAuthAccountsSnapshot(config, forceRefresh, options)).accounts;
453
461
  }
@@ -146,6 +146,11 @@ export async function fetchMainAccountInfo(forceRefresh = false, config?: OcxCon
146
146
  return (await fetchMainAccountInfoSnapshot(forceRefresh, config)).info;
147
147
  }
148
148
 
149
+ export interface MainAccountInfoProbeOptions {
150
+ /** Metadata-only renewal never sets or clears the traffic reauth quarantine. */
151
+ passive?: boolean;
152
+ }
153
+
149
154
  export const EMPTY_MAIN_ACCOUNT_INFO: MainAccountInfo = { email: null, plan: null, quota: null };
150
155
 
151
156
  export async function retryMainAccountInfoIfIdentityChanged(
@@ -154,12 +159,13 @@ export async function retryMainAccountInfoIfIdentityChanged(
154
159
  nativeMainLease: AdmissionLease,
155
160
  explicitRefresh: boolean,
156
161
  paced: boolean,
162
+ options: MainAccountInfoProbeOptions = {},
157
163
  ): Promise<MainAccountInfoFetchResult | null> {
158
164
  const currentAccountId = getMainChatgptAccountId();
159
165
  if (currentAccountId === null || currentAccountId === requestAccountId) return null;
160
166
  reconcileMainCodexAccountRuntimeState();
161
167
  return retriesRemaining > 0
162
- ? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease, explicitRefresh, false, paced)
168
+ ? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease, explicitRefresh, false, paced, options)
163
169
  : { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
164
170
  }
165
171
 
@@ -171,6 +177,7 @@ export async function fetchMainAccountInfoAttempt(
171
177
  explicitRefresh: boolean = forceRefresh,
172
178
  postReset = false,
173
179
  config?: OcxConfig,
180
+ options: MainAccountInfoProbeOptions = {},
174
181
  ): Promise<MainAccountInfoFetchResult> {
175
182
  const nativeMainLease = existingNativeMainLease ?? tryAcquireNativeMainProfileClaim();
176
183
  if (!nativeMainLease) {
@@ -184,7 +191,7 @@ export async function fetchMainAccountInfoAttempt(
184
191
  try {
185
192
  const paced = sharedMainQuotaPacingEnabled(config ?? loadConfig());
186
193
  const operation = async () => ({
187
- ...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease, explicitRefresh, postReset, paced),
194
+ ...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease, explicitRefresh, postReset, paced, options),
188
195
  identityGeneration: captureMainAccountIdentityGeneration(),
189
196
  });
190
197
  if (nativeMainSharedClaimHeld) return await operation();
@@ -225,6 +232,7 @@ export async function fetchMainAccountInfoWhileOwned(
225
232
  /** Reset-credit consume needs a fresh post-spend observation, not an earlier read. */
226
233
  postReset = false,
227
234
  paced = true,
235
+ options: MainAccountInfoProbeOptions = {},
228
236
  ): Promise<MainAccountInfoFetchResult> {
229
237
  const writerGeneration = captureConfigGeneration();
230
238
  reconcileMainCodexAccountRuntimeState();
@@ -287,7 +295,16 @@ export async function fetchMainAccountInfoWhileOwned(
287
295
  const current = credentialIsCurrent() && writerGeneration === captureConfigGeneration();
288
296
  if (!current) return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
289
297
  credentialChecked: true, hasCredential: true };
290
- if (explicitRefresh && (admission.result.quotaRefresh?.status === "ok"
298
+ // Single-flight shares evidence, but each caller retains its own auth behavior.
299
+ if (!options.passive && admission.result.terminalAuthFailure) {
300
+ const diagnosticStillLive = admission.result.quotaRefreshGeneration !== undefined
301
+ && isMainAccountIdentityGenerationLive(admission.result.quotaRefreshGeneration);
302
+ clearMainAccountInfoCache();
303
+ markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
304
+ return { ...admission.result, ...(diagnosticStillLive
305
+ ? { quotaRefreshGeneration: captureMainAccountIdentityGeneration() } : {}) };
306
+ }
307
+ if (!options.passive && explicitRefresh && (admission.result.quotaRefresh?.status === "ok"
291
308
  || admission.result.quotaRefresh?.status === "not_reported")) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
292
309
  return admission.result;
293
310
  }
@@ -297,13 +314,13 @@ export async function fetchMainAccountInfoWhileOwned(
297
314
  if (!resp.ok) {
298
315
  const authFailure = await classifyMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
299
316
  const terminalAuthFailure = authFailure.terminal;
300
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
317
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced, options);
301
318
  if (retried) return retried;
302
319
  if (!isQuotaDispatchCurrent(dispatchSequence) || !credentialIsCurrent()) {
303
320
  return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
304
321
  credentialChecked: true, hasCredential: true };
305
322
  }
306
- if (terminalAuthFailure) {
323
+ if (terminalAuthFailure && !options.passive) {
307
324
  // Account for this attempt's own synchronous invalidation, never prior external drift.
308
325
  const diagnosticStillLive = isMainAccountIdentityGenerationLive(quotaRefreshGeneration);
309
326
  clearMainAccountInfoCache();
@@ -324,7 +341,7 @@ export async function fetchMainAccountInfoWhileOwned(
324
341
  quotaPhase = "body";
325
342
  const data = (await resp.json()) as WhamUsageResponse;
326
343
  quotaPhase = "publish";
327
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
344
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced, options);
328
345
  if (retried) return retried;
329
346
  quotaPhase = "decode";
330
347
  if (data === null || typeof data !== "object" || Array.isArray(data)) {
@@ -376,7 +393,7 @@ export async function fetchMainAccountInfoWhileOwned(
376
393
  // never settled and the dashboard kept showing nothing — the symptom #327 reported.
377
394
  // An explicit refresh is an operator asking to re-evaluate, normally right after
378
395
  // signing in again, so it stays authoritative.
379
- if (explicitRefresh) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
396
+ if (explicitRefresh && !options.passive) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
380
397
  // Mirror main quota + plan into the shared stores so the rotation engine can
381
398
  // score and auto-switch the main account exactly like a pool account (Option A).
382
399
  setMainAccountPlan(result.plan);
@@ -398,7 +415,7 @@ export async function fetchMainAccountInfoWhileOwned(
398
415
  ...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
399
416
  };
400
417
  } catch (error) {
401
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
418
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced, options);
402
419
  if (retried) return retried;
403
420
  if (!credentialIsCurrent()) {
404
421
  return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
@@ -1,9 +1,11 @@
1
- import { codexAccountUsesCreditsAfterLimit } from "../account-credit-use";
1
+ import { codexAccountUsesCreditsAfterLimit, codexUsageLimitResetAt } from "../account-credit-use";
2
2
  import { nextCodexUsageQueryAt, nextQuotaQueryDelay, pruneRemovedCodexPoolUsageAccounts } from "../quota-query-backoff";
3
3
  import { CODEX_PRIORITY_FAILBACK_REFRESH_MS } from "../account-priority";
4
4
  import { codexQuotaHasFreshUsage } from "../quota-observation-freshness";
5
5
  import { getCodexAccountCredential, getValidCodexToken, readCodexAccountRecord } from "../account-store";
6
- import { getAccountQuota, isCompleteCodexQuotaRecoverySnapshot } from "../quota";
6
+ import { getAccountQuota, getMainPolicyQuota, isCompleteCodexQuotaRecoverySnapshot } from "../quota";
7
+ import { isCodexAccountPaused } from "../account-pause";
8
+ import { hasSpendableCodexCredits } from "../quota-types";
7
9
  import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
8
10
  import { claimDueCodexQuotaRecoveryProbes, settleCodexQuotaRecoveryProbe } from "../routing";
9
11
  import { readCodexTokens } from "../auth-collision";
@@ -73,17 +75,33 @@ export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Da
73
75
  return cooldownRecoveryInFlight;
74
76
  }
75
77
 
78
+ const MAIN_CREDIT_RENEWAL_AGE_MS = 3 * 60_000;
79
+
76
80
  let mainHardLockRecoveryInFlight: Promise<void> | null = null;
77
81
  let mainHardLockRecoveryAttempt: { identity: number; credential: number; after: number; delay: number } | undefined;
78
82
 
79
83
  /** Metadata-only recovery on the existing sweep; failures retain the observed policy block. */
80
84
  export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise<void> {
81
85
  if (mainHardLockRecoveryInFlight) return mainHardLockRecoveryInFlight;
86
+ /** Schedule a real credit observation while leaving actual-time admission unchanged. */
87
+ const creditRecoveryNeeded = (): boolean => {
88
+ const now = Date.now();
89
+ const quota = getMainPolicyQuota();
90
+ const observedAt = quota?.credits?.observedAt;
91
+ // getMainPolicyQuota binds evidence to the observed physical identity. Only renew
92
+ // valid formerly spendable funds; admission retains the actual five-minute clock.
93
+ return codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)
94
+ && !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
95
+ && codexUsageLimitResetAt(quota, undefined, now) !== undefined
96
+ && observedAt !== undefined && observedAt > 0
97
+ && now - observedAt >= MAIN_CREDIT_RENEWAL_AGE_MS
98
+ && hasSpendableCodexCredits(quota, observedAt);
99
+ };
82
100
  const status = getMainAccountHardLockStatus(config);
83
- if (status.state !== "blocked") { mainHardLockRecoveryAttempt = undefined; return; }
101
+ if (status.state !== "blocked" && !creditRecoveryNeeded()) { mainHardLockRecoveryAttempt = undefined; return; }
84
102
  if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
85
103
  // A predicted reset is a scheduling hint, never proof that the hard lock can be lifted.
86
- if (status.resetAt !== undefined && status.resetAt > Date.now()) return;
104
+ if (status.state === "blocked" && status.resetAt !== undefined && status.resetAt > Date.now()) return;
87
105
  // Reconcile the owned physical credential before skipping work for an older one.
88
106
  reconcileMainCodexAccountRuntimeState();
89
107
  const physical = readCodexTokens();
@@ -98,9 +116,10 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
98
116
  if (!lease) return;
99
117
  mainHardLockRecoveryInFlight = (async () => {
100
118
  reconcileMainCodexAccountRuntimeState();
101
- if (getMainAccountHardLockStatus(config).state !== "blocked"
119
+ if ((getMainAccountHardLockStatus(config).state !== "blocked" && !creditRecoveryNeeded())
102
120
  || isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
103
121
  const identityGeneration = captureMainAccountIdentityGeneration();
122
+ const passive = getMainAccountHardLockStatus(config).state !== "blocked";
104
123
  try {
105
124
  // Refresh can require an exclusive credential claim: never hold WHAM's shared
106
125
  // claim while obtaining a valid token. The runtime lease spans both operations.
@@ -111,7 +130,12 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
111
130
  // Native refresh retains its own grant-scoped refusal; global quarantine would outlive it.
112
131
  return;
113
132
  }
114
- if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
133
+ const currentStatus = getMainAccountHardLockStatus(config);
134
+ if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)
135
+ || !isMainAccountIdentityGenerationLive(identityGeneration)
136
+ || (passive && !creditRecoveryNeeded())
137
+ || (currentStatus.state === "blocked" && currentStatus.resetAt !== undefined
138
+ && currentStatus.resetAt > Date.now())) return;
115
139
  const credential = getMainQuotaCredentialGeneration();
116
140
  // Reconcile the physical bearer before consulting the previous credential's delay.
117
141
  if (previous?.identity === identityGeneration && previous.credential === credential
@@ -123,7 +147,7 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
123
147
  after: currentQueryAfter };
124
148
  return;
125
149
  }
126
- const result = await fetchMainAccountInfoAttempt(true, 1, lease, false, false, false, config);
150
+ const result = await fetchMainAccountInfoAttempt(true, 1, lease, false, false, false, config, { passive });
127
151
  // Never charge a replacement credential for a late result from its predecessor.
128
152
  if (isMainAccountIdentityGenerationLive(identityGeneration)
129
153
  && credential === getMainQuotaCredentialGeneration()) {
@@ -134,11 +158,11 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
134
158
  const authStatus = result.quotaRefresh.status === "http_error"
135
159
  ? result.quotaRefresh.httpStatus : undefined;
136
160
  const transientAuth = (authStatus === 401 || authStatus === 403)
137
- && !isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
161
+ && !result.terminalAuthFailure && !isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
138
162
  if (transientAuth) mainHardLockRecoveryAttempt = undefined;
139
163
  else {
140
164
  const delay = nextQuotaQueryDelay(previousDelay || undefined);
141
- mainHardLockRecoveryAttempt = getMainAccountHardLockStatus(config).state === "blocked"
165
+ mainHardLockRecoveryAttempt = getMainAccountHardLockStatus(config).state === "blocked" || creditRecoveryNeeded()
142
166
  ? { identity: identityGeneration, credential, delay, after: Math.max(Date.now() + delay, queryAfter) }
143
167
  : undefined;
144
168
  }
@@ -495,6 +495,7 @@ export async function fetchPoolAccountQuota(
495
495
  validatePending = false,
496
496
  afterDispatchSequence?: number,
497
497
  recoveryProbeNow?: number,
498
+ explicitRefresh = false,
498
499
  ): Promise<PoolQuotaResult> {
499
500
  const existing = getAccountQuota(accountId);
500
501
  if (afterDispatchSequence === undefined && !forceRefresh && existing && Date.now() - existing.updatedAt < POOL_CACHE_TTL) {
@@ -504,10 +505,23 @@ export async function fetchPoolAccountQuota(
504
505
  credentialGeneration: readCodexAccountRecord(accountId)?.generation,
505
506
  };
506
507
  }
508
+ const record = readCodexAccountRecord(accountId);
509
+ // The token endpoint already called this grant revoked or expired. Only a re-login or a
510
+ // completed validation rewrites the record and drops the marker, so until then a passive read
511
+ // (dashboard poll, listing, priming, recovery probe) would only POST the dead refresh token again
512
+ // and log the same 401. Report the stored verdict the failed refresh would have produced. An
513
+ // operator's explicit refresh command from any principal, a dashboard validation, and a post-reset
514
+ // readback keep their probe, and a source-linked credential spends no grant of its own, so none
515
+ // of them is held here.
516
+ if (!explicitRefresh && !validatePending && afterDispatchSequence === undefined && record && record.deletedAt == null
517
+ && record.credential && !record.credential.sourceAuthPath
518
+ && record.lastCodexValidationTerminal === true && record.lastCodexValidationStatus === "failed") {
519
+ markAccountNeedsReauth(accountId, captureConfigGeneration(), record.generation);
520
+ return { quota: existing ?? null, needsReauth: true, reauthReason: "refresh_failed", credentialGeneration: record.generation };
521
+ }
507
522
  // A token refresh may increment the generation (and rotate the refresh token) before WHAM
508
523
  // completes. Join a flight whose starting or resolved generation is still current, but let a
509
524
  // replacement credential with the same pool id start its own request.
510
- const record = readCodexAccountRecord(accountId);
511
525
  const flights = poolQuotaRefreshInFlight.get(accountId);
512
526
  const current = flights && [...flights].find(flight => {
513
527
  const generation = flight.state.resolvedCredentialGeneration
@@ -43,6 +43,7 @@ export async function handleCodexAuthAPI(
43
43
  // required by AGENTS_INSTALL.md. Raw-admin/CLI refreshes remain observational.
44
44
  return jsonResponse({ accounts: await listCodexAuthAccounts(config, true, {
45
45
  validatePending: principal === "gui-session",
46
+ explicitRefresh: true,
46
47
  }) });
47
48
  }
48
49
 
@@ -4,7 +4,7 @@ import type { OcxConfig } from "../types";
4
4
  import { getCodexRoutingKind, type CodexRoutingKind } from "./inject";
5
5
  import { collectRoutingAdoption, type RoutingAdoptionEvidence } from "./routing-adoption";
6
6
  import { diagnoseCodexShim, type CodexShimDiagnostic } from "./shim";
7
- import { diagnoseMacDesktopStartup, type DesktopStartupDiagnostic } from "../service/desktop-startup";
7
+ import { diagnoseDesktopStartup, type DesktopStartupDiagnostic } from "../service/desktop-startup";
8
8
 
9
9
  export type StartupProtection = "service" | "desktop" | "shim" | "none";
10
10
  export type StartupHealthStatus = "native" | "protected" | "at-risk";
@@ -90,7 +90,7 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
90
90
  // We can only credit an opencodex service/shim for routing that opencodex owns.
91
91
  // An arbitrary localhost gateway has an independent lifecycle that OCX cannot repair.
92
92
  const ownsLocalRouting = inputs.routingKind === "opencodex-local";
93
- const desktopEffective = inputs.platform === "darwin" && !inputs.diagnosticStale
93
+ const desktopEffective = (inputs.platform === "darwin" || inputs.platform === "linux") && !inputs.diagnosticStale
94
94
  && inputs.desktop?.owned === true && inputs.desktop.loginEnabled
95
95
  && inputs.desktop.running && inputs.desktop.viable;
96
96
  const protection: StartupProtection = ownsLocalRouting && inputs.serviceViable
@@ -147,7 +147,7 @@ export function collectStartupHealth(
147
147
  const shim = diagnostics.shim ?? diagnoseCodexShim();
148
148
  const service = diagnostics.service ?? diagnoseService();
149
149
  const routingKind = diagnostics.routingKind ?? getCodexRoutingKind();
150
- const desktop = diagnostics.desktop ?? diagnoseMacDesktopStartup();
150
+ const desktop = diagnostics.desktop ?? diagnoseDesktopStartup();
151
151
  const routingAdoption = diagnostics.routingAdoption
152
152
  ?? (routingKind === "opencodex-local" ? collectRoutingAdoption({ routingKind }) : undefined);
153
153
  return deriveStartupHealth({
@@ -1,3 +1,4 @@
1
+ import { isCodexControlPlaneModel } from "../control-plane-models";
1
2
  import { redactSecretString } from "../../lib/redact";
2
3
  import type { OcxConfig } from "../../types";
3
4
  import { encodeRoutedModelId } from "../../providers/slug-codec";
@@ -34,6 +35,7 @@ export type AutoReviewModelOverrideResult = "absent" | "applied" | "invalid" | "
34
35
 
35
36
  /** True when a catalog row was synthesized by opencodex instead of coming from upstream. */
36
37
  function isRoutedCatalogEntry(entry: RawEntry): boolean {
38
+ if (isCodexControlPlaneModel(entry.slug)) return false;
37
39
  const slug = typeof entry.slug === "string" ? entry.slug : "";
38
40
  return slug.includes("/")
39
41
  || (typeof entry.description === "string" && entry.description.startsWith("Routed via opencodex → "));
@@ -57,6 +59,7 @@ function clearAutoReviewOverrideValue(entry: RawEntry): void {
57
59
  * Returns the stamped values when the observed rows match that shape.
58
60
  */
59
61
  function legacyRootStampValues(observedModels: readonly RawEntry[]): ReadonlySet<string> | undefined {
62
+ observedModels = observedModels.filter(entry => !isCodexControlPlaneModel(entry?.slug));
60
63
  if (observedModels.some(entry => entry?.[AUTO_REVIEW_ROOT_MARKER] !== undefined)) return undefined;
61
64
  const configuredValues = new Set(observedModels.flatMap(entry => {
62
65
  const value = entry?.auto_review_model_override;
@@ -90,7 +93,7 @@ function clearLegacyRootStamps(models: readonly RawEntry[], sourceModels: readon
90
93
  const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
91
94
  if (legacyStamp === undefined) return;
92
95
  for (const entry of models) {
93
- if (!entry || typeof entry !== "object") continue;
96
+ if (!entry || typeof entry !== "object" || isCodexControlPlaneModel(entry.slug)) continue;
94
97
  const current = entry.auto_review_model_override;
95
98
  if (entry[AUTO_REVIEW_ROOT_MARKER] === undefined
96
99
  && typeof current === "string" && legacyStamp.has(current)) clearAutoReviewOverrideValue(entry);
@@ -107,7 +110,7 @@ function clearAutoReviewModelOverride(
107
110
  ): void {
108
111
  const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
109
112
  for (const entry of models) {
110
- if (!entry || typeof entry !== "object") continue;
113
+ if (!entry || typeof entry !== "object" || isCodexControlPlaneModel(entry.slug)) continue;
111
114
  const current = entry.auto_review_model_override;
112
115
  if (isRoutedCatalogEntry(entry)
113
116
  || (entry[AUTO_REVIEW_ROOT_MARKER] === true || rootAutoReviewStamp(entry) !== undefined)
@@ -188,6 +191,7 @@ function preserveNativeAutoReviewModelOverrides(
188
191
 
189
192
  /** Stamp a root-derived override and mark native rows so later root removal is durable. */
190
193
  function stampRootAutoReviewOverride(entry: RawEntry, target: string): void {
194
+ if (isCodexControlPlaneModel(entry.slug)) return;
191
195
  if (!isRoutedCatalogEntry(entry)) {
192
196
  const previous = rootAutoReviewStamp(entry);
193
197
  const current = entry.auto_review_model_override;
@@ -399,7 +403,7 @@ function applyRootSelectorToRemaining(
399
403
  ): AutoReviewModelOverrideResult {
400
404
  const clearRemaining = (): void => {
401
405
  for (const entry of models) {
402
- if (!entry || providerStamped.has(entry)) continue;
406
+ if (!entry || providerStamped.has(entry) || isCodexControlPlaneModel(entry.slug)) continue;
403
407
  // Native rows written by releases before the root marker cannot be told apart from upstream
404
408
  // values once provider stamps diverge. clearLegacyRootStamps sweeps the ones the legacy
405
409
  // uniform signature still recognizes before provider plans land, because provider stamping
@@ -427,7 +431,7 @@ function applyRootSelectorToRemaining(
427
431
  return "unresolved";
428
432
  }
429
433
  for (const entry of models) {
430
- if (!entry || providerStamped.has(entry)) continue;
434
+ if (!entry || providerStamped.has(entry) || isCodexControlPlaneModel(entry.slug)) continue;
431
435
  stampRootAutoReviewOverride(entry, trimmed);
432
436
  }
433
437
  return "applied";
@@ -449,7 +453,7 @@ export function applyConfiguredAutoReviewModelOverride(
449
453
  const { plans, failure } = buildProviderReviewPlans(models, config);
450
454
  const providerStamped = new Set<RawEntry>();
451
455
  for (const entry of models) {
452
- if (!entry || typeof entry !== "object") continue;
456
+ if (!entry || typeof entry !== "object" || isCodexControlPlaneModel(entry.slug)) continue;
453
457
  const provider = catalogEntryProviderName(entry);
454
458
  if (!provider) continue;
455
459
  const plan = plans.get(provider);
@@ -482,7 +486,10 @@ export function finalizeAutoReviewModelOverride(
482
486
  sourceModels: readonly RawEntry[] = [],
483
487
  config?: Pick<OcxConfig, "providers">,
484
488
  ): AutoReviewModelOverrideResult {
485
- if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(models, sourceModels);
489
+ if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(
490
+ models.filter(entry => !isCodexControlPlaneModel(entry.slug)),
491
+ sourceModels.filter(entry => !isCodexControlPlaneModel(entry.slug)),
492
+ );
486
493
  if (config && configHasProviderAutoReview(config)) {
487
494
  return applyConfiguredAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), config, sourceModels);
488
495
  }
@@ -1,3 +1,5 @@
1
+ import { isCodexControlPlaneModel } from "../control-plane-models";
2
+ import { withCodexControlPlaneRows } from "./control-plane";
1
3
  import { CODEX_REASONING_LEVELS, type CodexReasoningLevel } from "../../reasoning-effort";
2
4
  import { clearModelCache } from "../model-cache";
3
5
  import { routedSlug, slugEquivalenceKey } from "../../providers/slug-codec";
@@ -194,7 +196,7 @@ export function buildCatalogEntriesFromObservedState({
194
196
  const comboPublicSlugs = new Set(goModels
195
197
  .filter(model => model.provider === COMBO_NAMESPACE)
196
198
  .map(catalogModelSlug));
197
- for (const slug of gptSlugs) {
199
+ for (const slug of gptSlugs.filter(slug => !isCodexControlPlaneModel(slug))) {
198
200
  const native = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap);
199
201
  if (rank.has(slug)) native.priority = rank.get(slug)!;
200
202
  nativeEntries.push(native);
@@ -224,7 +226,7 @@ export function buildCatalogEntriesFromObservedState({
224
226
  const selectorNativeSlugs = accountNativeSlugsBySelector?.get(selector)
225
227
  ?? accountNativeSlugs
226
228
  ?? gptSlugs;
227
- const accountNativeEntries = selectorNativeSlugs.filter(slug => slug !== NATIVE_RESERVE_MODEL).map(slug => (
229
+ const accountNativeEntries = selectorNativeSlugs.filter(slug => slug !== NATIVE_RESERVE_MODEL && !isCodexControlPlaneModel(slug)).map(slug => (
228
230
  nativeEntriesBySlug.get(slug)
229
231
  ?? deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap)
230
232
  ));
@@ -306,7 +308,7 @@ export function buildCatalogEntriesFromObservedState({
306
308
  delete entry.prefer_websockets;
307
309
  }
308
310
  }
309
- return applyMultiAgentMode(out, multiAgentMode, multiAgentV2Enabled, {
311
+ return applyMultiAgentMode(withCodexControlPlaneRows(out, template ? [template] : [], true, wsEnabled), multiAgentMode, multiAgentV2Enabled, {
310
312
  keepNativeChatGptOnV1,
311
313
  preserveDefaultMultiAgentVersion: isReserveCatalogProjection,
312
314
  });
@@ -409,12 +411,12 @@ export function mergeCatalogModelsWithNativeRecovery(
409
411
  ): RawEntry[] {
410
412
  const merged = [...primaryCatalogModels];
411
413
  const recoveredNativeSlugs = new Set(primaryCatalogModels.flatMap(entry => {
412
- const slug = recoverableNativeSlug(entry);
414
+ const slug = isCodexControlPlaneModel(entry.slug) ? String(entry.slug) : recoverableNativeSlug(entry);
413
415
  return slug === null ? [] : [slug];
414
416
  }));
415
417
  for (const source of nativeRecoverySources) {
416
418
  for (const entry of source) {
417
- const slug = recoverableNativeSlug(entry);
419
+ const slug = isCodexControlPlaneModel(entry.slug) ? String(entry.slug) : recoverableNativeSlug(entry);
418
420
  if (slug === null || recoveredNativeSlugs.has(slug)) continue;
419
421
  merged.push(structuredClone(entry) as RawEntry);
420
422
  recoveredNativeSlugs.add(slug);
@@ -460,6 +462,7 @@ export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly s
460
462
  if (!pickerOrder.some(slug => !slug.includes("/"))) return;
461
463
  const rankOf = modelPickerRank(pickerOrder);
462
464
  for (const entry of entries) {
465
+ if (isCodexControlPlaneModel(entry.slug)) continue;
463
466
  const natural = entry[SPAWN_PRIORITY_FIELD] ?? entry.priority ?? 9;
464
467
  entry[SPAWN_PRIORITY_FIELD] = natural;
465
468
  entry.priority = rankOf(String(entry.slug)) ?? pickerOrder.length + Number(natural);
@@ -539,10 +542,12 @@ export function mergeCatalogEntriesFromObservedState({
539
542
  // Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at
540
543
  // the observed-core boundary so callers can safely retain evidence objects or repeat the merge.
541
544
  const detachedCatalogModels = catalogModels
545
+ .filter(entry => !isCodexControlPlaneModel(entry.slug))
542
546
  .map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry));
543
547
  const detachedBaselineCatalogModels = baselineCatalogModels
548
+ .filter(entry => !isCodexControlPlaneModel(entry.slug))
544
549
  .map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry));
545
- const detachedRoutedEntries = routedEntries.map(entry => structuredClone(entry) as RawEntry);
550
+ const detachedRoutedEntries = routedEntries.filter(entry => !isCodexControlPlaneModel(entry.slug)).map(entry => structuredClone(entry) as RawEntry);
546
551
  // Track this invocation's generated custom rows, not ownership markers read from disk.
547
552
  // Their builder already finalized exact native ladders and ordinary routed mock tiers.
548
553
  const freshCustomEntries = new Set(detachedRoutedEntries.filter(entry =>
@@ -921,7 +926,10 @@ export function mergeCatalogEntriesFromObservedState({
921
926
  // clobber a hide flag back to list. Bare ids disable every account clone; qualified ids disable
922
927
  // only their generated account row.
923
928
  const versionedEntries = applyMultiAgentMode(
924
- applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0, observedNativeSlugs),
929
+ withCodexControlPlaneRows(
930
+ applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0, observedNativeSlugs),
931
+ [...catalogModels, ...baselineCatalogModels, ...routedEntries], includeNativeOpenAi, wsEnabled,
932
+ ),
925
933
  multiAgentMode,
926
934
  multiAgentV2Enabled,
927
935
  { keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection, nativeDefaults: nativeMultiAgentDefaults },
@@ -0,0 +1,45 @@
1
+ import { CODEX_INTERNAL_OPENAI_MODELS, isCodexControlPlaneModel } from "../control-plane-models";
2
+ import { COMBO_NAMESPACE } from "../../combos";
3
+ import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
4
+ import { NATIVE_RESERVE_MODEL, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./native-models";
5
+ import { isReserveCatalogProjection } from "./reserve";
6
+ import { pinnedNativeModelRows } from "./pinned-models";
7
+ import type { RawEntry } from "./parsing";
8
+
9
+ /** Hidden ordinary natives qualify; Reserve, account choices and routed aliases do not. */
10
+ export function hasOrdinaryNativeOpenAiRow(entries: readonly RawEntry[]): boolean {
11
+ return entries.some(entry => typeof entry.slug === "string"
12
+ && !entry.slug.includes("/")
13
+ && SUPPORTED_NATIVE_OPENAI_SLUGS.has(entry.slug)
14
+ && entry.slug !== NATIVE_RESERVE_MODEL
15
+ && !isReserveCatalogProjection(entry)
16
+ && entry.opencodex_catalog_kind !== CODEX_NATIVE_ALIAS_CATALOG_KIND
17
+ && entry.owned_by !== COMBO_NAMESPACE);
18
+ }
19
+
20
+ /** Keep internal reviewer metadata separate from native synthesis, picker ordering and account clones. */
21
+ export function withCodexControlPlaneRows(
22
+ entries: RawEntry[],
23
+ sourceRows: readonly RawEntry[],
24
+ includeNativeOpenAi: boolean,
25
+ wsEnabled: boolean,
26
+ ): RawEntry[] {
27
+ const models = entries.filter(entry => !isCodexControlPlaneModel(entry.slug));
28
+ if (!includeNativeOpenAi || !hasOrdinaryNativeOpenAiRow(models)) return models;
29
+ for (const slug of CODEX_INTERNAL_OPENAI_MODELS) {
30
+ const source = sourceRows.find(entry => entry.slug === slug)
31
+ ?? pinnedNativeModelRows().find(entry => entry.slug === slug);
32
+ // A pin without the row degrades to Codex's own task-model fallback rather than failing the
33
+ // whole catalog write.
34
+ if (!source) continue;
35
+ const row: RawEntry = structuredClone(source);
36
+ row.visibility = "hide";
37
+ if (wsEnabled) row.supports_websockets = true;
38
+ else {
39
+ delete row.supports_websockets;
40
+ delete row.prefer_websockets;
41
+ }
42
+ models.push(row);
43
+ }
44
+ return models;
45
+ }