@bitkyc08/opencodex 2.81.0 → 2.83.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 (272) hide show
  1. package/bin/ocx.mjs +166 -73
  2. package/gui/dist/assets/App-Ba8uG696.js +54 -0
  3. package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
  4. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  5. package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
  6. package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic.ts +1 -1
  10. package/src/adapters/cursor/catalog.ts +2 -2
  11. package/src/adapters/cursor/effort-map.ts +1 -1
  12. package/src/adapters/devin/live-models.ts +2 -2
  13. package/src/adapters/google-truncation.ts +22 -0
  14. package/src/adapters/google.ts +7 -3
  15. package/src/adapters/ollama-native.ts +18 -14
  16. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  17. package/src/adapters/openai-responses/passthrough.ts +11 -4
  18. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  19. package/src/adapters/openai-responses/reasoning.ts +11 -1
  20. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  21. package/src/bridge/errors.ts +7 -1
  22. package/src/claude/agents-inject.ts +14 -3
  23. package/src/claude/context-windows.ts +22 -20
  24. package/src/claude/desktop-3p.ts +88 -12
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/desktop-profile.ts +2 -1
  27. package/src/claude/gateway-cache.ts +18 -1
  28. package/src/claude/inbound.ts +77 -3
  29. package/src/claude/intercept/cli-catalog.ts +3 -0
  30. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  31. package/src/claude/intercept/picker-models.ts +14 -6
  32. package/src/claude/intercept/settings.ts +3 -0
  33. package/src/claude/long-context.ts +66 -0
  34. package/src/claude/model-info.ts +19 -16
  35. package/src/claude/outbound.ts +94 -9
  36. package/src/claude/subagent-model.ts +40 -11
  37. package/src/cli/account-auth.ts +20 -12
  38. package/src/cli/agent.ts +9 -5
  39. package/src/cli/aside-profile-recovery.ts +33 -0
  40. package/src/cli/capabilities-base.ts +5 -5
  41. package/src/cli/capabilities-integrations.ts +2 -2
  42. package/src/cli/cli-path-diagnostics.ts +132 -0
  43. package/src/cli/desktop-runtime-guidance.ts +65 -0
  44. package/src/cli/dispatch.ts +5 -0
  45. package/src/cli/doctor.ts +16 -3
  46. package/src/cli/index.ts +14 -9
  47. package/src/cli/integration-aside-sync.ts +2 -1
  48. package/src/cli/integrations.ts +16 -3
  49. package/src/cli/resolve.ts +26 -3
  50. package/src/cli/root.ts +4 -2
  51. package/src/cli/status.ts +64 -12
  52. package/src/cli/update-restart-child.ts +4 -2
  53. package/src/cli/update-restart-home.ts +28 -10
  54. package/src/cli/update-restart-service-record.ts +116 -0
  55. package/src/cli/update-restart-supervision.ts +96 -0
  56. package/src/cli/update-restart.ts +59 -13
  57. package/src/cli/v2.ts +27 -20
  58. package/src/cli/version-skew-notice.ts +77 -0
  59. package/src/clients/config-export/model-metadata.ts +16 -0
  60. package/src/clients/config-export/zcode-store.ts +46 -11
  61. package/src/clients/config-export/zcode.ts +3 -5
  62. package/src/clients/config-export.ts +1 -1
  63. package/src/clients/lazycodex.ts +1 -1
  64. package/src/clients/omo-role-models.ts +32 -7
  65. package/src/codex/auth-api/account-selection.ts +107 -0
  66. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  67. package/src/codex/auth-api/routes.ts +10 -49
  68. package/src/codex/auth-context.ts +28 -4
  69. package/src/codex/autostart-health.ts +12 -3
  70. package/src/codex/catalog/build-entries.ts +18 -0
  71. package/src/codex/catalog/bundled.ts +155 -26
  72. package/src/codex/catalog/combo-member.ts +3 -2
  73. package/src/codex/catalog/effort.ts +26 -5
  74. package/src/codex/catalog/gather-capture.ts +5 -0
  75. package/src/codex/catalog/model-hints.ts +15 -8
  76. package/src/codex/catalog/model-visibility.ts +3 -1
  77. package/src/codex/catalog/native-models.ts +1 -1
  78. package/src/codex/catalog/operator-tiers.ts +38 -0
  79. package/src/codex/catalog/provider-models.ts +68 -35
  80. package/src/codex/catalog/retained-sync.ts +7 -5
  81. package/src/codex/catalog/routed-gather.ts +15 -1
  82. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  83. package/src/codex/catalog-auto-refresh.ts +19 -0
  84. package/src/codex/catalog.ts +1 -1
  85. package/src/codex/codex-home-owner.ts +3 -0
  86. package/src/codex/config-write-lock.ts +333 -0
  87. package/src/codex/convergence.ts +2 -1
  88. package/src/codex/desktop-app/darwin.ts +1 -1
  89. package/src/codex/desktop-app/handoff.ts +1 -1
  90. package/src/codex/desktop-app/linux.ts +1 -1
  91. package/src/codex/desktop-app/lock.ts +1 -1
  92. package/src/codex/desktop-app/types.ts +1 -1
  93. package/src/codex/desktop-app/windows.ts +1 -1
  94. package/src/codex/desktop-app-restart.ts +1 -1
  95. package/src/codex/features.ts +257 -200
  96. package/src/codex/inject/config-write-section.ts +34 -0
  97. package/src/codex/inject/multi-agent-v2.ts +14 -6
  98. package/src/codex/inject/remove.ts +36 -11
  99. package/src/codex/inject/restore.ts +85 -20
  100. package/src/codex/inject-coordination.ts +15 -6
  101. package/src/codex/inject.ts +62 -20
  102. package/src/codex/journal.ts +62 -17
  103. package/src/codex/main-account-cache.ts +43 -0
  104. package/src/codex/main-account.ts +6 -7
  105. package/src/codex/paths.ts +2 -0
  106. package/src/codex/prepared-home.ts +18 -0
  107. package/src/codex/prompt-journal.ts +25 -12
  108. package/src/codex/prompt-layers.ts +49 -26
  109. package/src/codex/prompt-lock-claim.ts +134 -0
  110. package/src/codex/prompt-lock-evidence.ts +26 -0
  111. package/src/codex/prompt-lock-io.ts +16 -0
  112. package/src/codex/prompt-lock-owner.ts +130 -0
  113. package/src/codex/prompt-lock.ts +84 -53
  114. package/src/codex/routing/failure-window.ts +146 -0
  115. package/src/codex/routing/selection.ts +4 -1
  116. package/src/codex/routing.ts +13 -17
  117. package/src/codex/runtime.ts +106 -18
  118. package/src/codex/shim.ts +45 -19
  119. package/src/combos/jev-service-exchange.ts +281 -0
  120. package/src/combos/jev.ts +34 -220
  121. package/src/config/atomic-write.ts +1 -1
  122. package/src/config/codex-ws-reuse-setting.ts +10 -0
  123. package/src/config/live-reconcile.ts +17 -0
  124. package/src/config/load-degrade.ts +8 -0
  125. package/src/config/rebase-provenance.ts +11 -0
  126. package/src/config/schema/config-schema.ts +4 -0
  127. package/src/generated/compatibility-version.json +375 -227
  128. package/src/generated/model-metadata.ts +2 -2
  129. package/src/grok/inject.ts +276 -206
  130. package/src/grok/reset-coupon-ledger.ts +223 -57
  131. package/src/grok/reset-coupons.ts +13 -6
  132. package/src/grok/sync.ts +2 -1
  133. package/src/images/loop.ts +29 -11
  134. package/src/integrations/aside-profiles.ts +10 -3
  135. package/src/integrations/catalog-refresh.ts +18 -2
  136. package/src/integrations/cursor-detect.ts +1 -1
  137. package/src/integrations/writer.ts +9 -3
  138. package/src/lib/bun-path-runtime.d.mts +32 -0
  139. package/src/lib/bun-path-runtime.mjs +95 -0
  140. package/src/lib/bun-runtime-preflight.ts +140 -0
  141. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  142. package/src/lib/desktop-cli-handoff.mjs +92 -0
  143. package/src/lib/desktop-cli-record.d.mts +29 -0
  144. package/src/lib/desktop-cli-record.mjs +102 -0
  145. package/src/lib/errors.ts +25 -0
  146. package/src/lib/redact.ts +2 -2
  147. package/src/lib/request-execution-budget.ts +8 -0
  148. package/src/lib/synced-state-location.ts +46 -6
  149. package/src/lib/test-home-guard.ts +87 -1
  150. package/src/lib/translator-budget.ts +17 -4
  151. package/src/lib/windows-atomic-replace.ts +38 -16
  152. package/src/lib/windows-elevation.ts +6 -0
  153. package/src/lib/windows-secret-acl.ts +92 -25
  154. package/src/lib/winsw.ts +2 -3
  155. package/src/oauth/anthropic-account-refusal.ts +39 -1
  156. package/src/oauth/anthropic-routing.ts +1 -1
  157. package/src/oauth/anthropic-send-ownership.ts +3 -3
  158. package/src/oauth/chatgpt.ts +113 -41
  159. package/src/oauth/index.ts +7 -3
  160. package/src/oauth/meta-muse-device.ts +1 -1
  161. package/src/oauth/store.ts +1 -1
  162. package/src/oauth/types.ts +1 -1
  163. package/src/providers/anthropic-fast.ts +1 -1
  164. package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
  165. package/src/providers/azure-model-metadata.ts +115 -0
  166. package/src/providers/derive.ts +21 -0
  167. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  168. package/src/providers/devin-provider-merge-migration.ts +1 -1
  169. package/src/providers/github-copilot-auto.ts +169 -0
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/muse-subscription-usage.ts +1 -1
  172. package/src/providers/quota/vendor-probes-key.ts +127 -17
  173. package/src/providers/quota.ts +1 -1
  174. package/src/providers/registry/entries-core.ts +6 -6
  175. package/src/providers/registry/entries-extended.ts +4 -0
  176. package/src/providers/registry/model-seeds.ts +3 -3
  177. package/src/providers/xai-fast-model.ts +1 -1
  178. package/src/responses/reasoning-envelope.ts +29 -1
  179. package/src/responses/reasoning-replay-cache.ts +17 -1
  180. package/src/responses/spill-store.ts +15 -2
  181. package/src/responses/state/metrics.ts +8 -0
  182. package/src/responses/state/spill-failure.ts +12 -0
  183. package/src/responses/state/spill-queue.ts +12 -5
  184. package/src/responses/state.ts +18 -10
  185. package/src/responses/task-input.ts +17 -0
  186. package/src/server/background-lifecycle.ts +27 -0
  187. package/src/server/chat-completions.ts +1 -0
  188. package/src/server/chat-native.ts +28 -12
  189. package/src/server/claude-messages.ts +43 -6
  190. package/src/server/inbound-body-admission.ts +25 -7
  191. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  192. package/src/server/index/link-listener.ts +30 -17
  193. package/src/server/index/serve-options.ts +18 -4
  194. package/src/server/index/spend-ledger-lifecycle.ts +2 -1
  195. package/src/server/index/startup-warnings.ts +19 -0
  196. package/src/server/index.ts +2 -1
  197. package/src/server/lifecycle.ts +31 -4
  198. package/src/server/management/agent-settings-routes.ts +93 -32
  199. package/src/server/management/codex-agent-role-routes.ts +4 -2
  200. package/src/server/management/codex-prompt-routes.ts +1 -0
  201. package/src/server/management/codex-role-auto-assign.ts +13 -2
  202. package/src/server/management/config-routes.ts +59 -8
  203. package/src/server/management/context.ts +2 -2
  204. package/src/server/management/grok-coupon-routes.ts +277 -52
  205. package/src/server/management/link-routes.ts +42 -20
  206. package/src/server/management/route-registry.ts +2 -2
  207. package/src/server/management/shared.ts +3 -3
  208. package/src/server/messages-native-oauth.ts +9 -8
  209. package/src/server/messages-native.ts +49 -13
  210. package/src/server/messages-response-headers.ts +14 -0
  211. package/src/server/relay-eager.ts +2 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-conversation.ts +48 -5
  214. package/src/server/request-log-failure-attribution.ts +3 -0
  215. package/src/server/request-log-terminal-status.ts +186 -0
  216. package/src/server/request-log.ts +15 -89
  217. package/src/server/responses/adapter-continuation.ts +11 -3
  218. package/src/server/responses/adapter-dispatch.ts +99 -19
  219. package/src/server/responses/codex-ws-exchange.ts +17 -2
  220. package/src/server/responses/codex-ws-pool.ts +100 -13
  221. package/src/server/responses/codex-ws-wire.ts +9 -0
  222. package/src/server/responses/combo-requested-effort.ts +3 -1
  223. package/src/server/responses/compact.ts +3 -2
  224. package/src/server/responses/core-codex-account.ts +22 -1
  225. package/src/server/responses/core-combo-native.ts +20 -3
  226. package/src/server/responses/core-combo.ts +22 -14
  227. package/src/server/responses/core-lifetime.ts +3 -36
  228. package/src/server/responses/core-opaque-recovery.ts +5 -5
  229. package/src/server/responses/core-options.ts +5 -0
  230. package/src/server/responses/core-replay.ts +73 -0
  231. package/src/server/responses/core.ts +9 -9
  232. package/src/server/responses/empty-completion-guard.ts +18 -2
  233. package/src/server/responses/fetch-helpers.ts +26 -3
  234. package/src/server/responses/jev-model-invoke.ts +1 -1
  235. package/src/server/responses/passthrough-delivery.ts +24 -2
  236. package/src/server/responses/passthrough-dispatch.ts +134 -62
  237. package/src/server/responses/passthrough-execution.ts +3 -3
  238. package/src/server/responses/request-prepare.ts +4 -0
  239. package/src/server/responses/request-send-budget.ts +112 -49
  240. package/src/server/responses/request-transport.ts +125 -5
  241. package/src/server/responses/response-effects.ts +1 -0
  242. package/src/server/responses/run-turn-execution.ts +15 -5
  243. package/src/server/responses/sidecar-execution.ts +30 -16
  244. package/src/server/responses/sidecar-send-budget.ts +58 -0
  245. package/src/server/responses/spend-storage-error.ts +33 -0
  246. package/src/server/responses/ws-upstream.ts +19 -6
  247. package/src/server/startup-health-cache.ts +2 -1
  248. package/src/server/workflow-refusal.ts +3 -0
  249. package/src/service/cli.ts +20 -4
  250. package/src/service/desktop-command-guard.ts +32 -0
  251. package/src/service/desktop-startup.ts +66 -40
  252. package/src/service/desktop-supervision.d.mts +26 -0
  253. package/src/service/desktop-supervision.mjs +140 -0
  254. package/src/service/managing-cli.ts +1 -0
  255. package/src/service/orchestration.ts +46 -20
  256. package/src/service/repair.ts +25 -9
  257. package/src/service/state.ts +4 -3
  258. package/src/service/systemd.ts +17 -8
  259. package/src/service/windows-ops.ts +61 -22
  260. package/src/storage/policy-job.ts +5 -1
  261. package/src/storage/policy-worker.ts +3 -1
  262. package/src/storage/policy.ts +9 -0
  263. package/src/types/config.ts +21 -0
  264. package/src/types/request.ts +10 -0
  265. package/src/types/wire.ts +1 -0
  266. package/src/update/index.ts +57 -9
  267. package/src/update/restart-ownership.ts +10 -3
  268. package/src/update/runtime-ownership.d.mts +6 -0
  269. package/src/update/runtime-ownership.mjs +23 -7
  270. package/src/usage/expected-prices.ts +1 -1
  271. package/src/web-search/loop.ts +5 -2
  272. package/gui/dist/assets/App-7yhoys8a.js +0 -54
@@ -0,0 +1,107 @@
1
+ import type { OcxConfig } from "../../types";
2
+ import { getConfigPath, initializePersistedConfigIfMissing, mutatePersistedConfig } from "../../config";
3
+ import { mergeConfigDefaults, readConfigFileSnapshot, validateConfigCandidate } from "../../config/diagnostics";
4
+ import { adoptPersistedCodexAccountSelection, claudeCodeBaselineArmed } from "../../config/live-reconcile";
5
+ import { deleteConfigTopLevelKey, fileBackedConfigPath, recordFileBackedConfig } from "../../config/rebase-provenance";
6
+ import { MAIN_CODEX_ACCOUNT_ID, hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount, isValidCodexAccountId } from "../account-id";
7
+ import { isCodexAccountPaused } from "../account-pause";
8
+ import { readCodexAccountRecord } from "../account-store";
9
+ import { codexAccountPinDrainReason, resetCodexRoutingForManualSelection } from "../routing";
10
+ import { jsonResponse } from "./http";
11
+
12
+ class SelectionRejected extends Error {
13
+ constructor(message: string, readonly status: number) { super(message); }
14
+ }
15
+
16
+ function validateSelection(config: OcxConfig, accountId: string | null): void {
17
+ if (accountId === null) return;
18
+ if (accountId === MAIN_CODEX_ACCOUNT_ID && hasLegacyMainCodexPoolAccount(config.codexAccounts)) {
19
+ throw new SelectionRejected("Remove the legacy __main__ pool row before selecting the Desktop account", 409);
20
+ }
21
+ if (isCodexAccountPaused(config, accountId)) throw new SelectionRejected("Account is paused", 409);
22
+ if (accountId === MAIN_CODEX_ACCOUNT_ID) return;
23
+ if (!isValidCodexAccountId(accountId)) throw new SelectionRejected("Invalid account id format", 400);
24
+ if (!(config.codexAccounts ?? []).some(account => isSelectableCodexPoolAccount(account) && account.id === accountId)) {
25
+ throw new SelectionRejected("Account not found", 400);
26
+ }
27
+ if (readCodexAccountRecord(accountId)?.codexValidationPending) {
28
+ throw new SelectionRejected("Account validation is pending. Refresh quota after recovery to validate it.", 409);
29
+ }
30
+ }
31
+
32
+ function applySelection(config: OcxConfig, accountId: string | null): boolean {
33
+ const changed = config.activeCodexAccountId !== (accountId ?? undefined)
34
+ || config.activeCodexAccountPinned !== (accountId ?? undefined);
35
+ if (accountId === null) {
36
+ deleteConfigTopLevelKey(config, "activeCodexAccountId");
37
+ deleteConfigTopLevelKey(config, "activeCodexAccountPinned");
38
+ } else {
39
+ config.activeCodexAccountId = accountId;
40
+ config.activeCodexAccountPinned = accountId;
41
+ }
42
+ return changed;
43
+ }
44
+
45
+ /** A manual selection is an explicit command, even when equal to the live merge baseline. */
46
+ export function persistCodexAccountSelection(config: OcxConfig, accountId: string | null): Response {
47
+ const unknown = () => jsonResponse({
48
+ error: "Account selection could not be confirmed; reload settings before retrying",
49
+ code: "account_selection_unavailable",
50
+ }, 409);
51
+ const configPath = getConfigPath();
52
+ const sourcePath = fileBackedConfigPath(config);
53
+ if (sourcePath !== undefined && sourcePath !== configPath) return unknown();
54
+ let committed: OcxConfig;
55
+ let warning = false;
56
+ try {
57
+ // Do not activate a newly disk-only account that the running proxy has not adopted.
58
+ validateSelection(config, accountId);
59
+ const before = readConfigFileSnapshot();
60
+ if (before.diagnostics.source === "default" && !fileBackedConfigPath(config) && !claudeCodeBaselineArmed(config)) {
61
+ // Preserve transient/initial callers without recreating a deleted live configuration.
62
+ // The create-only owner cannot replace a competing writer's newly published config.
63
+ const normalized = validateConfigCandidate(mergeConfigDefaults(config));
64
+ if (!normalized.ok) return unknown();
65
+ const initial = normalized.config;
66
+ applySelection(initial, accountId);
67
+ initializePersistedConfigIfMissing(initial);
68
+ }
69
+ const result = mutatePersistedConfig(persisted => {
70
+ // This callback is re-run under the mutation lock immediately before publication.
71
+ // A removed or paused account must not be resurrected by a stale UI snapshot.
72
+ validateSelection(config, accountId);
73
+ validateSelection(persisted, accountId);
74
+ return { changed: applySelection(persisted, accountId), value: persisted };
75
+ });
76
+ if (result.status === "unavailable") return unknown();
77
+ committed = result.value;
78
+ } catch (error) {
79
+ if (error instanceof SelectionRejected) return jsonResponse({ error: error.message }, error.status);
80
+ // Atomic publication may succeed before generation/bookkeeping fails. Confirm the exact
81
+ // durable selection instead of rolling live state back behind an already published write.
82
+ const snapshot = readConfigFileSnapshot();
83
+ if (snapshot.raw === undefined) return unknown();
84
+ try {
85
+ const strict = validateConfigCandidate(JSON.parse(snapshot.raw.replace(/^\uFEFF/, "")));
86
+ if (!strict.ok) return unknown();
87
+ validateSelection(config, accountId);
88
+ validateSelection(strict.config, accountId);
89
+ if (strict.config.activeCodexAccountId !== (accountId ?? undefined)
90
+ || strict.config.activeCodexAccountPinned !== (accountId ?? undefined)) return unknown();
91
+ committed = strict.config;
92
+ warning = true;
93
+ } catch { return unknown(); }
94
+ }
95
+ adoptPersistedCodexAccountSelection(config, committed);
96
+ recordFileBackedConfig(config, configPath);
97
+ // No failed save may clear affinity, spend the manual preference, or move the RR cursor.
98
+ resetCodexRoutingForManualSelection(accountId ?? MAIN_CODEX_ACCOUNT_ID);
99
+ const pinDrainReason = accountId === null ? undefined : codexAccountPinDrainReason(config, accountId);
100
+ return jsonResponse({
101
+ ok: true,
102
+ activeCodexAccountId: config.activeCodexAccountId ?? null,
103
+ appliesImmediately: true,
104
+ ...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
105
+ ...(warning ? { warning: "config_bookkeeping_failed" } : {}),
106
+ });
107
+ }
@@ -88,14 +88,13 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
88
88
  const now = Date.now();
89
89
  const quota = getMainPolicyQuota();
90
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.
91
+ // Positive evidence renews before expiry; unavailable evidence keeps discovery
92
+ // eligible under the same pacing. Admission still requires fresh spendable funds.
93
93
  return codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)
94
94
  && !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
95
95
  && codexUsageLimitResetAt(quota, undefined, now) !== undefined
96
- && observedAt !== undefined && observedAt > 0
97
- && now - observedAt >= MAIN_CREDIT_RENEWAL_AGE_MS
98
- && hasSpendableCodexCredits(quota, observedAt);
96
+ && (!hasSpendableCodexCredits(quota, now)
97
+ || (observedAt !== undefined && now - observedAt >= MAIN_CREDIT_RENEWAL_AGE_MS));
99
98
  };
100
99
  const status = getMainAccountHardLockStatus(config);
101
100
  if (status.state !== "blocked" && !creditRecoveryNeeded()) { mainHardLockRecoveryAttempt = undefined; return; }
@@ -1,3 +1,4 @@
1
+ import { persistCodexAccountSelection } from "./account-selection";
1
2
  import { CODEX_ACCOUNT_LOG_LABEL_RE, codexAccountLogLabel } from "../account-label";
2
3
  import { poolQuotaHistoryIdentity, readCodexAccountRecord } from "../account-store";
3
4
  import { estimateCodexQuotaCapacity, insufficientCodexCapacity } from "../quota-capacity";
@@ -7,13 +8,13 @@ import { getAccountQuotaHistory, listAccountQuotas } from "../quota";
7
8
  import { deleteCodexAccount } from "../account-lifecycle";
8
9
  import { isCodexAccountPaused, setCodexAccountPaused } from "../account-pause";
9
10
  import { setAllCodexAccountsCreditsAfterLimit, setCodexAccountCreditsAfterLimit } from "../account-credit-use";
10
- import { clearCodexAccountPin, isCodexAccountPriorityKey, pinnedCodexAccountId, setCodexAccountPin, setCodexAccountPriority } from "../account-priority";
11
- import { codexAccountPinDrainReason, codexQuotaScopeForModel, clearCodexAccountCooldown, clearThreadAccountMapForAccount, getEffectiveActiveCodexAccountId, isEffectiveCodexAccountPinned, resetCodexRoutingForManualSelection } from "../routing";
11
+ import { clearCodexAccountPin, isCodexAccountPriorityKey, pinnedCodexAccountId, setCodexAccountPriority } from "../account-priority";
12
+ import { codexQuotaScopeForModel, clearCodexAccountCooldown, clearThreadAccountMapForAccount, getEffectiveActiveCodexAccountId, isEffectiveCodexAccountPinned } from "../routing";
12
13
  import { DEFAULT_ACCOUNT_PRIORITY, MAX_ACCOUNT_PRIORITY, MIN_ACCOUNT_PRIORITY, normalizeAccountPoolStickyLimit, normalizeCodexAccountPoolStrategy, parseAccountPoolStickyLimit, parseCodexAccountPoolStrategy, parseAccountPriority } from "../pool-rotation";
13
14
  import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
14
15
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
15
16
  import type { OcxConfig } from "../../types";
16
- import { CODEX_ACCOUNT_ID_RE, hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount, isValidCodexAccountId } from "../account-id";
17
+ import { CODEX_ACCOUNT_ID_RE, isSelectableCodexPoolAccount, isValidCodexAccountId } from "../account-id";
17
18
  import { isCodexResetCreditOperationId } from "../reset-credit-recovery";
18
19
  import { listCodexAuthAccounts, selectFallbackAfterPause, pauseExhaustedCodexAccounts } from "./account-list";
19
20
  import { jsonResponse, manualImportDisabledResponse } from "./http";
@@ -247,53 +248,13 @@ export async function handleCodexAuthAPI(
247
248
  }
248
249
 
249
250
  if (url.pathname === "/api/codex-auth/active" && req.method === "PUT") {
250
- let body: { accountId: string | null };
251
- try { body = (await req.json()) as typeof body; } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
252
- const runtimeConfig = getRuntimeConfig(config);
253
- const targetAccountId = body.accountId ?? MAIN_CODEX_ACCOUNT_ID;
254
- if (body.accountId === MAIN_CODEX_ACCOUNT_ID && hasLegacyMainCodexPoolAccount(runtimeConfig.codexAccounts)) {
255
- return jsonResponse({ error: "Remove the legacy __main__ pool row before selecting the Desktop account" }, 409);
256
- }
257
- if (body.accountId != null && isCodexAccountPaused(runtimeConfig, targetAccountId)) {
258
- return jsonResponse({ error: "Account is paused" }, 409);
259
- }
260
- if (body.accountId != null && body.accountId !== MAIN_CODEX_ACCOUNT_ID) {
261
- if (!isValidCodexAccountId(body.accountId)) return jsonResponse({ error: "Invalid account id format" }, 400);
262
- const exists = (runtimeConfig.codexAccounts ?? [])
263
- .some(account => isSelectableCodexPoolAccount(account) && account.id === body.accountId);
264
- if (!exists) return jsonResponse({ error: "Account not found" }, 400);
265
- if (readCodexAccountRecord(body.accountId)?.codexValidationPending) {
266
- return jsonResponse({ error: "Account validation is pending. Refresh quota after recovery to validate it." }, 409);
267
- }
251
+ let body: unknown;
252
+ try { body = await req.json(); } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
253
+ if (!body || typeof body !== "object" || Array.isArray(body)
254
+ || !("accountId" in body) || (body.accountId !== null && typeof body.accountId !== "string")) {
255
+ return jsonResponse({ error: "accountId must be an account id or null" }, 400);
268
256
  }
269
- runtimeConfig.activeCodexAccountId = body.accountId ?? undefined;
270
- // "Use this account now" outranks selection order until the account is spent:
271
- // persisted here rather than in resetCodexRoutingForManualSelection, which is
272
- // runtime state only. A null id clears the selection instead of making one, so it
273
- // must release the pin rather than record one: pinning the `targetAccountId`
274
- // fallback would leave a pin that no effective active account matches, which
275
- // `isEffectiveCodexAccountPinned` reports as unpinned while the tier filter still
276
- // honours it as a ceiling — invisibly capping the pool at the main account's tier.
277
- if (body.accountId == null) clearCodexAccountPin(runtimeConfig);
278
- else setCodexAccountPin(runtimeConfig, targetAccountId);
279
- resetCodexRoutingForManualSelection(targetAccountId);
280
- saveRuntimeConfig(config, runtimeConfig);
281
- // A pin this route accepts can still be dropped by the very next resolve, and saying
282
- // nothing about that is what made the setting look ignored (#4521). The checks above
283
- // refuse an account that cannot be selected at all; this reports the one remaining
284
- // outcome they do not cover, from the same predicate routing releases on, so the two
285
- // cannot drift. Absent means the pin survives — additive for existing clients.
286
- // `appliesImmediately` is unchanged: it answers whether thread affinity was cleared,
287
- // not whether the pin is durable.
288
- const pinDrainReason = body.accountId == null
289
- ? undefined
290
- : codexAccountPinDrainReason(runtimeConfig, targetAccountId);
291
- return jsonResponse({
292
- ok: true,
293
- activeCodexAccountId: body.accountId,
294
- appliesImmediately: true,
295
- ...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
296
- });
257
+ return persistCodexAccountSelection(getRuntimeConfig(config), body.accountId);
297
258
  }
298
259
 
299
260
  if (url.pathname === "/api/codex-auth/active" && req.method === "GET") {
@@ -1,5 +1,6 @@
1
1
  import { bindPoolCreditPolicy, poolCreditHoldResetAt, poolContextCreditHoldResetAt } from "./pool-credit-policy";
2
2
  import { hasSpendableCodexCredits } from "./quota-types";
3
+ import { nextQuotaQueryDelay } from "./quota-query-backoff";
3
4
  import { noteMainAccountActivity } from "./main-account-external-usage";
4
5
  import { codexAccountPriorityFailbackEnabled } from "./account-priority";
5
6
  import type { PoolQuotaWriter } from "./quota-types";
@@ -81,11 +82,13 @@ import {
81
82
  import {
82
83
  captureMainAccountIdentityGeneration,
83
84
  captureMainQuotaWriter,
85
+ captureMainQuotaDispatch,
84
86
  getObservedMainQuotaIdentityKey,
85
87
  isMainQuotaWriterLive,
86
88
  matchesMainQuotaCredential,
87
89
  observeMainQuotaCredential,
88
90
  type MainQuotaWriter,
91
+ type MainQuotaDispatch,
89
92
  } from "./main-account-cache";
90
93
  import { CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, isCodexReserveHelperUnsupported, isCodexReserveRequestEligible } from "./loopback-target";
91
94
  import type { DataPlaneAdmission } from "../server/auth-cors";
@@ -275,7 +278,11 @@ export function previewCodexPoolLineage(
275
278
  }
276
279
 
277
280
  export type CodexAuthContext =
278
- | { kind: "main"; accountId: null; reserveAuthorization?: MainReserveAuthorization }
281
+ | {
282
+ kind: "main"; accountId: null; reserveAuthorization?: MainReserveAuthorization;
283
+ /** Captured for the observed main credential actually selected for upstream. */
284
+ mainQuotaDispatch?: MainQuotaDispatch;
285
+ }
279
286
  | {
280
287
  kind: "pool";
281
288
  accountId: string;
@@ -547,8 +554,11 @@ function assertPoolAccountCredits(accountId: string, resetAt: number | undefined
547
554
  export class CodexMainAccountCreditsOffError extends CodexAccountCooldownError {
548
555
  readonly resetAt?: number;
549
556
 
550
- constructor(resetAt?: number) {
551
- super(MAIN_CODEX_ACCOUNT_ID, resetAt ?? 0);
557
+ constructor(resetAt?: number, creditRecovery = false) {
558
+ // Keep the included reset as policy evidence, but let consented callers retry
559
+ // within the base metadata recovery interval even when backoff grows longer.
560
+ super(MAIN_CODEX_ACCOUNT_ID, creditRecovery
561
+ ? Math.min(resetAt ?? Infinity, Date.now() + nextQuotaQueryDelay()) : resetAt ?? 0);
552
562
  this.name = "CodexMainAccountCreditsOffError";
553
563
  this.resetAt = resetAt;
554
564
  this.message = "Codex main account reached its usage limit, and spending ChatGPT credits is off or no fresh spendable balance is available."
@@ -772,7 +782,8 @@ function assertMainAccountPolicy(
772
782
  const status = getMainAccountHardLockStatus(config);
773
783
  if (status.state === "blocked") throw new CodexMainAccountHardLockError(status.resetAt, status.thresholds);
774
784
  const creditsResetAt = mainCreditsHoldResetAt(config);
775
- if (creditsResetAt !== undefined) throw new CodexMainAccountCreditsOffError(creditsResetAt);
785
+ if (creditsResetAt !== undefined) throw new CodexMainAccountCreditsOffError(creditsResetAt,
786
+ codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID));
776
787
  }
777
788
  // Only an admitted request is opencodex's own use of the main account. Counting a refused one
778
789
  // would hide outside usage from the warning exactly while the lock is holding.
@@ -1635,6 +1646,14 @@ export class CodexMainSubstitutionUnavailableError extends Error {
1635
1646
  }
1636
1647
  }
1637
1648
 
1649
+ /** Dispatch identity comes only from an owned observation of the selected credential. */
1650
+ function selectedMainQuotaDispatch(selected: Headers): MainQuotaDispatch | undefined {
1651
+ const bearer = selected.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
1652
+ if (!bearer) return undefined;
1653
+ const accountId = selected.get("chatgpt-account-id") ?? extractAccountId(undefined, bearer);
1654
+ return captureMainQuotaDispatch(bearer, accountId, captureConfigGeneration());
1655
+ }
1656
+
1638
1657
  /**
1639
1658
  * Build the upstream auth headers for one Codex turn.
1640
1659
  *
@@ -1654,6 +1673,7 @@ export function materializeCodexUpstreamAuth(
1654
1673
  ctx: CodexAuthContext,
1655
1674
  options: CodexAuthMaterializationOptions = {},
1656
1675
  ): Headers {
1676
+ if (ctx.kind === "main") ctx.mainQuotaDispatch = undefined;
1657
1677
  const selected = new Headers();
1658
1678
  for (const name of FORWARD_HEADERS) {
1659
1679
  const value = headers.get(name);
@@ -1695,10 +1715,12 @@ export function materializeCodexUpstreamAuth(
1695
1715
  observeSelectedMainCredential(stored, writer);
1696
1716
  assertMainAccountPolicy(options.config);
1697
1717
  assertMaterializedReserve(selected, ctx, options);
1718
+ ctx.mainQuotaDispatch = selectedMainQuotaDispatch(selected);
1698
1719
  return selected;
1699
1720
  }
1700
1721
  if (callerMatchesObservedMain(selected)) assertMainAccountPolicy(options.config);
1701
1722
  assertMaterializedReserve(selected, ctx, options);
1723
+ if (ctx.kind === "main") ctx.mainQuotaDispatch = selectedMainQuotaDispatch(selected);
1702
1724
  return selected;
1703
1725
  }
1704
1726
 
@@ -1749,6 +1771,7 @@ export async function materializeCodexUpstreamAuthAsync(
1749
1771
  ctx: CodexAuthContext,
1750
1772
  options: CodexAuthMaterializationOptions = {},
1751
1773
  ): Promise<Headers> {
1774
+ if (ctx.kind === "main") ctx.mainQuotaDispatch = undefined;
1752
1775
  if (requiresReserveAuthorization(options.config, options.modelId, options.admission)) {
1753
1776
  return materializeReserveUpstreamAuth(headers, ctx, options);
1754
1777
  }
@@ -1776,6 +1799,7 @@ export async function materializeCodexUpstreamAuthAsync(
1776
1799
  assertMainAccountPolicy(options.config);
1777
1800
  // An opt-in enabled during token refresh must not turn a proof-less context into Reserve.
1778
1801
  assertMaterializedReserve(selected, ctx, options);
1802
+ ctx.mainQuotaDispatch = selectedMainQuotaDispatch(selected);
1779
1803
  return selected;
1780
1804
  }
1781
1805
 
@@ -60,6 +60,7 @@ export interface StartupHealth {
60
60
  platform: NodeJS.Platform;
61
61
  diagnosticStale: boolean;
62
62
  recommendedCommand: string | null;
63
+ recommendedAction?: string | null;
63
64
  commands: {
64
65
  installService: string;
65
66
  repairService: string;
@@ -91,7 +92,8 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
91
92
  // An arbitrary localhost gateway has an independent lifecycle that OCX cannot repair.
92
93
  const ownsLocalRouting = inputs.routingKind === "opencodex-local";
93
94
  const desktopEffective = (inputs.platform === "darwin" || inputs.platform === "linux") && !inputs.diagnosticStale
94
- && inputs.desktop?.owned === true && inputs.desktop.loginEnabled
95
+ && inputs.desktop !== undefined && (inputs.desktop.owned || inputs.desktop.supervisor !== undefined)
96
+ && inputs.desktop.loginEnabled
95
97
  && inputs.desktop.running && inputs.desktop.viable;
96
98
  const protection: StartupProtection = ownsLocalRouting && inputs.serviceViable
97
99
  ? "service"
@@ -105,7 +107,7 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
105
107
  : rebootSafe
106
108
  ? "protected"
107
109
  : "at-risk";
108
- const recommendedCommand = status !== "at-risk" || (ownsLocalRouting && inputs.desktop?.owned)
110
+ const recommendedCommand = status !== "at-risk" || (ownsLocalRouting && inputs.desktop !== undefined)
109
111
  ? null
110
112
  : inputs.routingKind === "custom-local" || inputs.routingKind === "unknown"
111
113
  ? COMMANDS.restoreNative
@@ -117,6 +119,11 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
117
119
  // gets the registering command.
118
120
  ? (inputs.serviceInstalled && !inputs.serviceConflict ? COMMANDS.repairService : COMMANDS.installService)
119
121
  : COMMANDS.restoreNative;
122
+ const recommendedAction = status === "at-risk" && ownsLocalRouting && inputs.desktop !== undefined
123
+ ? inputs.diagnosticStale || inputs.desktop.loginEnabled
124
+ ? "Reopen OpenCodex and check Start at Login."
125
+ : "Turn on Start at Login in the OpenCodex menu so the desktop app starts this proxy after a restart."
126
+ : null;
120
127
  return {
121
128
  ...inputs,
122
129
  diagnosticStale: inputs.diagnosticStale ?? false,
@@ -127,6 +134,7 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
127
134
  protection,
128
135
  shimCoverage,
129
136
  recommendedCommand,
137
+ recommendedAction,
130
138
  commands: { ...COMMANDS },
131
139
  };
132
140
  }
@@ -211,7 +219,8 @@ function classifyStartupHealthSummary(health: StartupHealth): string {
211
219
  const command = health.recommendedCommand ?? health.commands.restoreNative;
212
220
  if (health.routingKind === "unknown") return `AT RISK after restart (Codex routing could not be verified; run '${command}')`;
213
221
  if (health.routingKind === "custom-local") return `AT RISK after restart (custom local gateway lifecycle is not managed by opencodex; run '${command}')`;
214
- if (health.desktop?.owned) return "AT RISK after restart (desktop startup could not be verified; reopen OpenCodex and check Start at Login)";
222
+ if (health.desktop?.supervisor && !health.desktop.loginEnabled && !health.diagnosticStale) return "AT RISK after restart (OpenCodex Desktop runs this proxy, but its Start at Login could not be verified; turn it on in the OpenCodex menu)";
223
+ if (health.desktop) return "AT RISK after restart (desktop startup could not be verified; reopen OpenCodex and check Start at Login)";
215
224
  if (health.shimCoverage === "cli-only") return `AT RISK for Codex Desktop after restart (launcher shim covers CLI scripts only; run '${command}')`;
216
225
  if (health.serviceConflict) return `AT RISK after restart (background service managers conflict; run '${command}')`;
217
226
  if (health.serviceStale) return `AT RISK after restart (background service files are stale; run '${command}')`;
@@ -48,6 +48,7 @@ import { NATIVE_RESERVE_MODEL } from "./native-models";
48
48
  import { isReserveCatalogProjection, type ReserveCatalogProjection } from "./reserve";
49
49
  import { deriveEntry, finishUpstreamNativeEntry, isExactComboCatalogEntry } from "./derive-entry";
50
50
  import { PICKER_ORDER_PRIORITY_BASE, SPAWN_PRIORITY_FIELD } from "./subagent-roster";
51
+ import { preserveOperatorUltraFastTiers } from "./operator-tiers";
51
52
 
52
53
  export interface ObservedCatalogEntryBuildInput {
53
54
  readonly template: RawEntry | null;
@@ -470,6 +471,8 @@ export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly s
470
471
  }
471
472
 
472
473
  export interface ObservedCatalogMergeInput {
474
+ /** Preserve operator-supplied Ultra Fast only from the exact persisted routed row. */
475
+ readonly ultraFastTier?: boolean;
473
476
  readonly catalogModels: readonly RawEntry[];
474
477
  readonly baselineCatalogModels: readonly RawEntry[];
475
478
  readonly routedEntries: readonly RawEntry[];
@@ -510,6 +513,7 @@ export interface ObservedCatalogMergeInput {
510
513
  * accidentally fall back to process-ambient catalog discovery or merge-policy warnings.
511
514
  */
512
515
  export function mergeCatalogEntriesFromObservedState({
516
+ ultraFastTier = false,
513
517
  catalogModels,
514
518
  baselineCatalogModels,
515
519
  routedEntries,
@@ -849,6 +853,20 @@ export function mergeCatalogEntriesFromObservedState({
849
853
  }
850
854
  return false;
851
855
  });
856
+ // First occurrence wins, matching enforceCatalogSlugUniqueness: a later duplicate row of the
857
+ // same slug is what the writer drops, so it must not decide which declaration survives.
858
+ const operatorRows = new Map<string, RawEntry | undefined>();
859
+ for (const entry of detachedCatalogModels) {
860
+ if (typeof entry.slug !== "string" || operatorRows.has(entry.slug)) continue;
861
+ operatorRows.set(entry.slug, trustedAccountBoundNativeCatalogSlug(entry) === undefined
862
+ && !isNativeAliasCatalogEntry(entry) && entry.owned_by !== COMBO_NAMESPACE ? entry : undefined);
863
+ }
864
+ for (const entry of finalRoutedEntries) {
865
+ if (typeof entry.slug !== "string" || !entry.slug.includes("/")
866
+ || isNativeAliasCatalogEntry(entry) || isExactComboCatalogEntry(entry, exactComboSlugs)
867
+ || trustedAccountBoundNativeCatalogSlug(entry) !== undefined) continue;
868
+ preserveOperatorUltraFastTiers(entry, operatorRows.get(entry.slug), ultraFastTier);
869
+ }
852
870
  const finalRoutedEntrySet = new Set(finalRoutedEntries);
853
871
  const degradedPreservedCount = preservedRoutedEntries.filter(entry => {
854
872
  if (!finalRoutedEntrySet.has(entry)) return false;